theWitcher commited on
Commit
a561836
·
verified ·
1 Parent(s): 15eb87a

update to read fron .json files

Browse files
Files changed (1) hide show
  1. index.html +366 -127
index.html CHANGED
@@ -11,7 +11,47 @@
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"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
16
  <style>
17
  @import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600;700&display=swap');
@@ -1637,6 +1677,7 @@
1637
  };
1638
 
1639
  // DOM Elements
 
1640
  const toolsContainer = document.getElementById('toolsContainer');
1641
  const videosContainer = document.getElementById('videosContainer');
1642
  const searchInput = document.getElementById('searchInput');
@@ -1654,180 +1695,342 @@
1654
  const saveJsonBtn = document.getElementById('saveJsonBtn');
1655
  const showToolsBtn = document.getElementById('showToolsBtn');
1656
  const showVideosBtn = document.getElementById('showVideosBtn');
1657
- const editJsonBtn = document.getElementById('editJsonBtn');
1658
- const editJsonBtnMobile = document.getElementById('editJsonBtnMobile');
1659
  const refreshBtn = document.getElementById('refreshBtn');
1660
  const refreshBtnMobile = document.getElementById('refreshBtnMobile');
1661
 
1662
  // State
1663
  let currentCategory = 'all';
1664
  let currentSearchTerm = '';
1665
- let toolsData = JSON.parse(JSON.stringify(defaultToolsData));
1666
 
1667
- // Initialize
1668
- function init() {
1669
- loadData();
1670
  renderTools();
1671
- renderVideos();
1672
  updateStats();
1673
  setupEventListeners();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1674
  }
1675
 
1676
- // Load data from localStorage or use default
1677
- function loadData() {
1678
  const savedData = localStorage.getItem('aiToolsData');
1679
  if (savedData) {
1680
- toolsData = JSON.parse(savedData);
 
 
 
 
 
 
 
 
 
 
 
 
1681
  } else {
1682
- toolsData = JSON.parse(JSON.stringify(defaultToolsData));
1683
- saveData();
1684
  }
1685
  }
1686
 
1687
  // Save data to localStorage
1688
  function saveData() {
1689
- localStorage.setItem('aiToolsData', JSON.stringify(toolsData));
 
 
 
 
 
1690
  }
1691
 
1692
- // Render tools based on filters
1693
  function renderTools() {
 
 
 
 
1694
  const filteredTools = filterTools();
1695
-
1696
- toolsContainer.innerHTML = '';
1697
-
1698
- filteredTools.forEach(tool => {
1699
- const toolCard = document.createElement('div');
1700
- toolCard.className = `tool-card bg-white rounded-lg shadow-sm border border-gray-100 p-6 transition duration-300 ${tool.isFeatured ? 'ring-2 ring-blue-500' : ''}`;
1701
-
1702
- toolCard.innerHTML = `
1703
- <div class="flex items-start mb-4">
1704
- <div class="p-3 rounded-lg ${getCategoryColor(tool.category)} text-white mr-4">
1705
- <i class="${tool.icon} text-xl"></i>
1706
- </div>
1707
- <div>
1708
- <h3 class="text-xl font-semibold">${tool.name}</h3>
1709
- <span class="text-xs px-2 py-1 rounded-full ${getCategoryBadgeColor(tool.category)}">${getCategoryName(tool.category)}</span>
 
 
 
 
 
1710
  </div>
1711
- </div>
1712
- <p class="text-gray-700 mb-4">${tool.description}</p>
1713
- <div class="flex justify-between items-center">
1714
- <div class="flex">
1715
- ${renderRatingStars(tool.rating)}
 
1716
  </div>
1717
- ${tool.isNew ? '<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full">חדש!</span>' : ''}
1718
- </div>
1719
- <a href="${tool.url}" target="_blank" class="mt-4 inline-block w-full text-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
1720
- <i class="fas fa-external-link-alt ml-2"></i> גישה לכלי
1721
- </a>
1722
- `;
1723
-
1724
- toolsContainer.appendChild(toolCard);
1725
- });
 
 
 
 
 
 
1726
  }
1727
 
1728
- // Render videos
1729
  function renderVideos() {
1730
- videosContainer.innerHTML = '';
1731
-
1732
- toolsData.videos.forEach(video => {
1733
- const videoCard = document.createElement('div');
1734
- videoCard.className = 'bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden';
1735
-
1736
- videoCard.innerHTML = `
1737
- <div class="relative pt-[56.25%]">
1738
- <iframe class="absolute top-0 left-0 w-full h-full" src="${video.url}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
1739
- </div>
1740
- <div class="p-4">
1741
- <h3 class="text-lg font-semibold mb-2">${video.title}</h3>
1742
- <p class="text-gray-600 text-sm mb-3">${video.description}</p>
1743
- <p class="text-gray-500 text-xs">${formatDate(video.date)}</p>
1744
- </div>
1745
- `;
1746
-
1747
- videosContainer.appendChild(videoCard);
1748
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  }
1750
 
1751
- // Filter tools based on category and search term
1752
  function filterTools() {
 
 
 
1753
  return toolsData.tools.filter(tool => {
1754
- const matchesCategory = currentCategory === 'all' || tool.category === currentCategory;
1755
- const matchesSearch = tool.name.toLowerCase().includes(currentSearchTerm.toLowerCase()) ||
1756
- tool.description.toLowerCase().includes(currentSearchTerm.toLowerCase());
1757
- return matchesCategory && matchesSearch;
 
 
 
 
1758
  });
1759
  }
1760
 
1761
- // Update statistics
1762
  function updateStats() {
1763
- totalToolsElement.textContent = toolsData.tools.length;
1764
-
1765
- const topRatedCount = toolsData.tools.filter(tool => tool.rating >= 4).length;
 
1766
  topRatedElement.textContent = topRatedCount;
1767
-
1768
- const newToolsCount = toolsData.tools.filter(tool => tool.isNew).length;
1769
  newToolsElement.textContent = newToolsCount;
1770
-
1771
- const categories = new Set(toolsData.tools.map(tool => tool.category));
1772
  totalCategoriesElement.textContent = categories.size;
1773
  }
1774
 
1775
- // Render rating stars
1776
  function renderRatingStars(rating) {
1777
- let stars = '';
1778
- for (let i = 1; i <= 5; i++) {
1779
- if (i <= rating) {
1780
- stars += '<i class="fas fa-star text-yellow-400"></i>';
1781
- } else {
1782
- stars += '<i class="far fa-star text-yellow-400"></i>';
1783
- }
1784
- }
1785
- return stars;
 
1786
  }
1787
 
1788
- // Get category name in Hebrew
1789
  function getCategoryName(category) {
1790
  const categories = {
1791
  'productivity': 'פרודוקטיביות',
1792
  'writing': 'כתיבה',
1793
  'design': 'עיצוב',
1794
  'coding': 'תכנות',
1795
- 'video': 'וידאו'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1796
  };
1797
- return categories[category] || category;
1798
  }
1799
 
1800
- // Get category color
1801
  function getCategoryColor(category) {
1802
  const colors = {
1803
  'productivity': 'bg-blue-600',
1804
  'writing': 'bg-purple-600',
1805
  'design': 'bg-pink-600',
1806
  'coding': 'bg-green-600',
1807
- 'video': 'bg-red-600'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1808
  };
1809
- return colors[category] || 'bg-gray-600';
1810
  }
1811
 
1812
- // Get category badge color
1813
  function getCategoryBadgeColor(category) {
1814
  const colors = {
1815
  'productivity': 'bg-blue-100 text-blue-800',
1816
  'writing': 'bg-purple-100 text-purple-800',
1817
  'design': 'bg-pink-100 text-pink-800',
1818
  'coding': 'bg-green-100 text-green-800',
1819
- 'video': 'bg-red-100 text-red-800'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1820
  };
1821
- return colors[category] || 'bg-gray-100 text-gray-800';
1822
  }
1823
 
1824
- // Format date
1825
  function formatDate(dateString) {
1826
- const options = { year: 'numeric', month: 'long', day: 'numeric' };
1827
- return new Date(dateString).toLocaleDateString('he-IL', options);
 
 
 
 
 
 
 
 
 
 
 
 
1828
  }
1829
 
1830
- // Setup event listeners
 
1831
  function setupEventListeners() {
1832
  // Search input
1833
  searchInput.addEventListener('input', (e) => {
@@ -1850,16 +2053,18 @@
1850
  mobileMenu.classList.toggle('open');
1851
  });
1852
 
1853
- // JSON Editor Modal
1854
- editJsonBtn.addEventListener('click', () => {
1855
- jsonEditor.value = JSON.stringify(toolsData.tools, null, 2);
 
1856
  jsonEditorModal.classList.remove('hidden');
1857
- });
1858
 
1859
- editJsonBtnMobile.addEventListener('click', () => {
1860
- jsonEditor.value = JSON.stringify(toolsData.tools, null, 2);
1861
- jsonEditorModal.classList.remove('hidden');
1862
- });
 
1863
 
1864
  closeModalBtn.addEventListener('click', () => {
1865
  jsonEditorModal.classList.add('hidden');
@@ -1873,15 +2078,18 @@
1873
  try {
1874
  const newData = JSON.parse(jsonEditor.value);
1875
  if (showToolsBtn.classList.contains('bg-blue-600')) {
 
1876
  toolsData.tools = newData;
1877
  } else {
 
1878
  toolsData.videos = newData;
1879
  }
1880
- saveData();
1881
  renderTools();
1882
- renderVideos();
1883
  updateStats();
1884
  jsonEditorModal.classList.add('hidden');
 
1885
  } catch (e) {
1886
  alert('JSON לא תקין: ' + e.message);
1887
  }
@@ -1903,26 +2111,57 @@
1903
  jsonEditor.value = JSON.stringify(toolsData.videos, null, 2);
1904
  });
1905
 
1906
- // Refresh buttons
1907
- refreshBtn.addEventListener('click', () => {
1908
- toolsData = JSON.parse(JSON.stringify(defaultToolsData));
1909
- saveData();
1910
- renderTools();
1911
- renderVideos();
1912
- updateStats();
1913
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1914
 
1915
- refreshBtnMobile.addEventListener('click', () => {
1916
- toolsData = JSON.parse(JSON.stringify(defaultToolsData));
1917
- saveData();
1918
- renderTools();
1919
- renderVideos();
1920
- updateStats();
1921
- });
1922
  }
1923
 
1924
- // Initialize the app
1925
  document.addEventListener('DOMContentLoaded', init);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1926
  </script>
1927
- <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=theWitcher/sagi-ai-tools" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1928
  </html>
 
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');
 
1677
  };
1678
 
1679
  // DOM Elements
1680
+ // DOM Elements (נשארים ללא שינוי - רק לרפרנס)
1681
  const toolsContainer = document.getElementById('toolsContainer');
1682
  const videosContainer = document.getElementById('videosContainer');
1683
  const searchInput = document.getElementById('searchInput');
 
1695
  const saveJsonBtn = document.getElementById('saveJsonBtn');
1696
  const showToolsBtn = document.getElementById('showToolsBtn');
1697
  const showVideosBtn = document.getElementById('showVideosBtn');
1698
+ // const editJsonBtn = document.getElementById('editJsonBtn'); // הלחצן מוסתר כרגע
1699
+ // const editJsonBtnMobile = document.getElementById('editJsonBtnMobile'); // הלחצן מוסתר כרגע
1700
  const refreshBtn = document.getElementById('refreshBtn');
1701
  const refreshBtnMobile = document.getElementById('refreshBtnMobile');
1702
 
1703
  // State
1704
  let currentCategory = 'all';
1705
  let currentSearchTerm = '';
1706
+ let toolsData = { tools: [], videos: [] }; // Initialize empty
1707
 
1708
+ // Initialize - Make it async
1709
+ async function init() {
1710
+ await loadData(); // Wait for data to load
1711
  renderTools();
1712
+ renderVideos(); // Call the specific renderVideos function
1713
  updateStats();
1714
  setupEventListeners();
1715
+ // Set initial active filter button
1716
+ const allFilterBtn = document.querySelector('.filter-btn[data-category="all"]');
1717
+ if (allFilterBtn) {
1718
+ allFilterBtn.classList.add('active');
1719
+ }
1720
+ }
1721
+
1722
+ // Fetch default data from JSON files
1723
+ async function fetchDefaultData() {
1724
+ try {
1725
+ const [toolsResponse, videosResponse] = await Promise.all([
1726
+ fetch('tools.json'),
1727
+ fetch('videos.json')
1728
+ ]);
1729
+
1730
+ if (!toolsResponse.ok || !videosResponse.ok) {
1731
+ throw new Error(`HTTP error! status: ${toolsResponse.status} / ${videosResponse.status}`);
1732
+ }
1733
+
1734
+ const [toolsArray, videosArray] = await Promise.all([
1735
+ toolsResponse.json(),
1736
+ videosResponse.json()
1737
+ ]);
1738
+
1739
+ return { tools: toolsArray, videos: videosArray };
1740
+ } catch (error) {
1741
+ console.error("Failed to fetch default data:", error);
1742
+ alert("שגיאה בטעינת נתוני ברירת המחדל. נסה לרענן את הדף.");
1743
+ return { tools: [], videos: [] }; // Return empty structure on error
1744
+ }
1745
  }
1746
 
1747
+ // Load data from localStorage or fetch default from JSON
1748
+ async function loadData() {
1749
  const savedData = localStorage.getItem('aiToolsData');
1750
  if (savedData) {
1751
+ try {
1752
+ toolsData = JSON.parse(savedData);
1753
+ // Basic validation: ensure it has tools and videos arrays
1754
+ if (!Array.isArray(toolsData.tools) || !Array.isArray(toolsData.videos)) {
1755
+ console.warn("Invalid data structure in localStorage. Fetching defaults.");
1756
+ toolsData = await fetchDefaultData();
1757
+ saveData(); // Save the fetched defaults
1758
+ }
1759
+ } catch (e) {
1760
+ console.error("Error parsing data from localStorage:", e);
1761
+ toolsData = await fetchDefaultData(); // Fetch defaults if parsing fails
1762
+ saveData(); // Save the fetched defaults
1763
+ }
1764
  } else {
1765
+ toolsData = await fetchDefaultData(); // Fetch defaults if no saved data
1766
+ saveData(); // Save the fetched defaults to localStorage
1767
  }
1768
  }
1769
 
1770
  // Save data to localStorage
1771
  function saveData() {
1772
+ try {
1773
+ localStorage.setItem('aiToolsData', JSON.stringify(toolsData));
1774
+ } catch (e) {
1775
+ console.error("Error saving data to localStorage:", e);
1776
+ alert("שגיאה בשמירת הנתונים. ייתכן שהאחסון המקומי מלא.");
1777
+ }
1778
  }
1779
 
1780
+ // Render tools based on filters (Modified slightly for safety)
1781
  function renderTools() {
1782
+ if (!toolsContainer || !toolsData || !Array.isArray(toolsData.tools)) {
1783
+ console.error("Cannot render tools: Missing container or invalid data.");
1784
+ return;
1785
+ }
1786
  const filteredTools = filterTools();
1787
+
1788
+ toolsContainer.innerHTML = ''; // Clear previous tools
1789
+
1790
+ if (filteredTools.length === 0) {
1791
+ toolsContainer.innerHTML = '<p class="text-center text-gray-500 col-span-full">לא נמצאו כלים התואמים את החיפוש או הסינון.</p>';
1792
+ } else {
1793
+ filteredTools.forEach(tool => {
1794
+ const toolCard = document.createElement('div');
1795
+ // Add relative positioning for potential future badges (like admin edit)
1796
+ 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' : ''}`;
1797
+
1798
+ toolCard.innerHTML = `
1799
+ <div class="flex items-start mb-4">
1800
+ <div class="p-3 rounded-lg ${getCategoryColor(tool.category)} text-white mr-4 flex-shrink-0">
1801
+ <i class="${tool.icon || 'fas fa-question-circle'} text-xl"></i>
1802
+ </div>
1803
+ <div class="flex-grow">
1804
+ <h3 class="text-xl font-semibold">${tool.name || 'שם לא ידוע'}</h3>
1805
+ <span class="text-xs px-2 py-1 rounded-full ${getCategoryBadgeColor(tool.category)}">${getCategoryName(tool.category || 'כללי')}</span>
1806
+ </div>
1807
  </div>
1808
+ <p class="text-gray-700 mb-4 text-sm min-h-[60px]">${tool.description || 'אין תיאור זמין.'}</p>
1809
+ <div class="flex justify-between items-center mb-4">
1810
+ <div class="flex">
1811
+ ${renderRatingStars(tool.rating || 0)}
1812
+ </div>
1813
+ ${tool.isNew ? '<span class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">חדש!</span>' : ''}
1814
  </div>
1815
+ <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' : ''}">
1816
+ <i class="fas fa-external-link-alt ml-2"></i> ${tool.url ? 'גישה לכלי' : 'אין קישור'}
1817
+ </a>
1818
+ `;
1819
+
1820
+ toolsContainer.appendChild(toolCard);
1821
+ });
1822
+ }
1823
+ }
1824
+
1825
+ // Helper to get YouTube ID (Keep as is)
1826
+ function getYouTubeID(url) {
1827
+ if (!url) return '';
1828
+ const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
1829
+ return match ? match[1] : '';
1830
  }
1831
 
1832
+ // Render videos (Keep mostly as is, just ensure data source is correct)
1833
  function renderVideos() {
1834
+ if (!videosContainer || !toolsData || !Array.isArray(toolsData.videos)) {
1835
+ console.error("Cannot render videos: Missing container or invalid data.");
1836
+ return; // Prevent errors if data isn't ready or invalid
1837
+ }
1838
+
1839
+ videosContainer.innerHTML = ''; // Clear previous videos
1840
+
1841
+ if (toolsData.videos.length === 0) {
1842
+ videosContainer.innerHTML = '<p class="text-center text-gray-500 col-span-full">אין סרטונים להצגה.</p>';
1843
+ } else {
1844
+ toolsData.videos.forEach(video => {
1845
+ const videoId = getYouTubeID(video.url);
1846
+ // If we can't get an ID, maybe skip or show a placeholder? For now, generate embed URL anyway.
1847
+ const embedUrl = videoId ? `https://www.youtube.com/embed/${videoId}` : '#'; // Use '#' or a placeholder URL if ID is missing
1848
+
1849
+ const videoCard = document.createElement('div');
1850
+ videoCard.className = 'bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden';
1851
+
1852
+ videoCard.innerHTML = `
1853
+ <div class="relative pt-[56.25%] ${!videoId ? 'bg-gray-200 flex items-center justify-center' : ''}">
1854
+ ${videoId ? `
1855
+ <iframe class="absolute top-0 left-0 w-full h-full" src="${embedUrl}" frameborder="0"
1856
+ title="${video.title || 'YouTube video'}"
1857
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
1858
+ allowfullscreen></iframe>
1859
+ ` : `
1860
+ <p class="text-gray-500 text-sm">קישור וידאו לא תקין</p>
1861
+ `}
1862
+ </div>
1863
+ <div class="p-4">
1864
+ <h3 class="text-lg font-semibold mb-2">${video.title || 'כותרת חסרה'}</h3>
1865
+ <p class="text-gray-600 text-sm mb-3 min-h-[40px]">${video.description || 'אין תיאור זמין.'}</p>
1866
+ <p class="text-gray-500 text-xs">${formatDate(video.date)}</p>
1867
+ </div>
1868
+ `;
1869
+
1870
+ videosContainer.appendChild(videoCard);
1871
+ });
1872
+ }
1873
  }
1874
 
1875
+ // Filter tools (Modified slightly for safety)
1876
  function filterTools() {
1877
+ if (!toolsData || !Array.isArray(toolsData.tools)) {
1878
+ return []; // Return empty array if data is not ready
1879
+ }
1880
  return toolsData.tools.filter(tool => {
1881
+ const nameMatch = tool.name && tool.name.toLowerCase().includes(currentSearchTerm.toLowerCase());
1882
+ const descMatch = tool.description && tool.description.toLowerCase().includes(currentSearchTerm.toLowerCase());
1883
+ const categoryMatch = tool.category && tool.category.toLowerCase().includes(currentSearchTerm.toLowerCase()); // Optional: search category name too
1884
+ const matchesSearch = nameMatch || descMatch || categoryMatch;
1885
+
1886
+ const matchesCategoryFilter = currentCategory === 'all' || tool.category === currentCategory;
1887
+
1888
+ return matchesCategoryFilter && matchesSearch;
1889
  });
1890
  }
1891
 
1892
+ // Update statistics (Modified slightly for safety)
1893
  function updateStats() {
1894
+ const toolsCount = (toolsData && Array.isArray(toolsData.tools)) ? toolsData.tools.length : 0;
1895
+ totalToolsElement.textContent = toolsCount;
1896
+
1897
+ const topRatedCount = toolsCount > 0 ? toolsData.tools.filter(tool => tool.rating >= 4).length : 0;
1898
  topRatedElement.textContent = topRatedCount;
1899
+
1900
+ const newToolsCount = toolsCount > 0 ? toolsData.tools.filter(tool => tool.isNew).length : 0;
1901
  newToolsElement.textContent = newToolsCount;
1902
+
1903
+ const categories = toolsCount > 0 ? new Set(toolsData.tools.map(tool => tool.category)) : new Set();
1904
  totalCategoriesElement.textContent = categories.size;
1905
  }
1906
 
1907
+ // Render rating stars (Keep as is)
1908
  function renderRatingStars(rating) {
1909
+ let stars = '';
1910
+ const filledStars = Math.max(0, Math.min(5, Math.round(rating || 0))); // Ensure rating is between 0 and 5
1911
+ for (let i = 1; i <= 5; i++) {
1912
+ if (i <= filledStars) {
1913
+ stars += '<i class="fas fa-star text-yellow-400"></i>';
1914
+ } else {
1915
+ stars += '<i class="far fa-star text-gray-300"></i>'; // Use gray for empty stars
1916
+ }
1917
+ }
1918
+ return stars;
1919
  }
1920
 
1921
+ // Get category name (Keep as is, maybe add a default)
1922
  function getCategoryName(category) {
1923
  const categories = {
1924
  'productivity': 'פרודוקטיביות',
1925
  'writing': 'כתיבה',
1926
  'design': 'עיצוב',
1927
  'coding': 'תכנות',
1928
+ 'video': 'וידאו',
1929
+ 'image': 'תמונה',
1930
+ 'education': 'חינוך',
1931
+ 'data': 'נתונים',
1932
+ 'search': 'חיפוש',
1933
+ 'builder': 'בנייה',
1934
+ 'customer-support': 'תמיכה',
1935
+ 'automation': 'אוטומציה',
1936
+ 'hosting': 'אחסון',
1937
+ 'agents': 'סוכנים',
1938
+ 'directory': 'אינדקס',
1939
+ 'utility': 'כלי עזר',
1940
+ 'platform': 'פלטפורמה',
1941
+ 'media': 'מדיה',
1942
+ 'presentation': 'מצגות',
1943
+ 'audio': 'שמע',
1944
+ 'infrastructure': 'תשתיות',
1945
+ 'nlp': 'עיבוד שפה',
1946
+ 'accessibility': 'נגישות'
1947
+ // Add other categories from your JSON here
1948
  };
1949
+ return categories[category] || category || 'כללי'; // Return category key or 'כללי' if null/undefined
1950
  }
1951
 
1952
+ // Get category color (Add more colors or a default)
1953
  function getCategoryColor(category) {
1954
  const colors = {
1955
  'productivity': 'bg-blue-600',
1956
  'writing': 'bg-purple-600',
1957
  'design': 'bg-pink-600',
1958
  'coding': 'bg-green-600',
1959
+ 'video': 'bg-red-600',
1960
+ 'image': 'bg-yellow-600',
1961
+ 'education': 'bg-indigo-600',
1962
+ 'data': 'bg-cyan-600',
1963
+ 'search': 'bg-teal-600',
1964
+ 'builder': 'bg-orange-600',
1965
+ 'customer-support': 'bg-lime-600',
1966
+ 'automation': 'bg-sky-600',
1967
+ 'hosting': 'bg-amber-600',
1968
+ 'agents': 'bg-violet-600',
1969
+ 'directory': 'bg-fuchsia-600',
1970
+ 'utility': 'bg-rose-600',
1971
+ 'platform': 'bg-emerald-600',
1972
+ 'media': 'bg-stone-600',
1973
+ 'presentation': 'bg-red-500',
1974
+ 'audio': 'bg-blue-500',
1975
+ 'infrastructure': 'bg-gray-700',
1976
+ 'nlp': 'bg-purple-500',
1977
+ 'accessibility': 'bg-green-500'
1978
+ // Add more as needed
1979
  };
1980
+ return colors[category] || 'bg-gray-600'; // Default color
1981
  }
1982
 
1983
+ // Get category badge color (Add more or a default)
1984
  function getCategoryBadgeColor(category) {
1985
  const colors = {
1986
  'productivity': 'bg-blue-100 text-blue-800',
1987
  'writing': 'bg-purple-100 text-purple-800',
1988
  'design': 'bg-pink-100 text-pink-800',
1989
  'coding': 'bg-green-100 text-green-800',
1990
+ 'video': 'bg-red-100 text-red-800',
1991
+ 'image': 'bg-yellow-100 text-yellow-800',
1992
+ 'education': 'bg-indigo-100 text-indigo-800',
1993
+ 'data': 'bg-cyan-100 text-cyan-800',
1994
+ 'search': 'bg-teal-100 text-teal-800',
1995
+ 'builder': 'bg-orange-100 text-orange-800',
1996
+ 'customer-support': 'bg-lime-100 text-lime-800',
1997
+ 'automation': 'bg-sky-100 text-sky-800',
1998
+ 'hosting': 'bg-amber-100 text-amber-800',
1999
+ 'agents': 'bg-violet-100 text-violet-800',
2000
+ 'directory': 'bg-fuchsia-100 text-fuchsia-800',
2001
+ 'utility': 'bg-rose-100 text-rose-800',
2002
+ 'platform': 'bg-emerald-100 text-emerald-800',
2003
+ 'media': 'bg-stone-100 text-stone-800',
2004
+ 'presentation': 'bg-red-100 text-red-800',
2005
+ 'audio': 'bg-blue-100 text-blue-800',
2006
+ 'infrastructure': 'bg-gray-200 text-gray-800',
2007
+ 'nlp': 'bg-purple-100 text-purple-800',
2008
+ 'accessibility': 'bg-green-100 text-green-800'
2009
+ // Add more as needed
2010
  };
2011
+ return colors[category] || 'bg-gray-100 text-gray-800'; // Default badge color
2012
  }
2013
 
2014
+ // Format date (Keep as is, maybe add error handling)
2015
  function formatDate(dateString) {
2016
+ if (!dateString) return 'תאריך לא זמין';
2017
+ try {
2018
+ // Handle potential non-standard date formats if necessary
2019
+ const date = new Date(dateString);
2020
+ // Check if date is valid
2021
+ if (isNaN(date.getTime())) {
2022
+ return 'תאריך לא תקין';
2023
+ }
2024
+ const options = { year: 'numeric', month: 'long', day: 'numeric' };
2025
+ return date.toLocaleDateString('he-IL', options);
2026
+ } catch (e) {
2027
+ console.error("Error formatting date:", dateString, e);
2028
+ return 'תאריך לא תקין';
2029
+ }
2030
  }
2031
 
2032
+
2033
+ // Setup event listeners (Update refresh button logic)
2034
  function setupEventListeners() {
2035
  // Search input
2036
  searchInput.addEventListener('input', (e) => {
 
2053
  mobileMenu.classList.toggle('open');
2054
  });
2055
 
2056
+ // JSON Editor Modal (Keep as is, but consider removing buttons if not admin)
2057
+ const openEditorHandler = () => {
2058
+ // Default to showing tools first
2059
+ showToolsBtn.click(); // Simulate click to set initial state
2060
  jsonEditorModal.classList.remove('hidden');
2061
+ };
2062
 
2063
+ // If Edit buttons exist, add listeners
2064
+ // const editJsonBtn = document.getElementById('editJsonBtn');
2065
+ // const editJsonBtnMobile = document.getElementById('editJsonBtnMobile');
2066
+ // if (editJsonBtn) editJsonBtn.addEventListener('click', openEditorHandler);
2067
+ // if (editJsonBtnMobile) editJsonBtnMobile.addEventListener('click', openEditorHandler);
2068
 
2069
  closeModalBtn.addEventListener('click', () => {
2070
  jsonEditorModal.classList.add('hidden');
 
2078
  try {
2079
  const newData = JSON.parse(jsonEditor.value);
2080
  if (showToolsBtn.classList.contains('bg-blue-600')) {
2081
+ if (!Array.isArray(newData)) throw new Error("Data must be an array.");
2082
  toolsData.tools = newData;
2083
  } else {
2084
+ if (!Array.isArray(newData)) throw new Error("Data must be an array.");
2085
  toolsData.videos = newData;
2086
  }
2087
+ saveData(); // Save changes to localStorage
2088
  renderTools();
2089
+ renderVideos(); // Re-render videos as well
2090
  updateStats();
2091
  jsonEditorModal.classList.add('hidden');
2092
+ alert("הנתונים נשמרו בהצלחה (באחסון המקומי).");
2093
  } catch (e) {
2094
  alert('JSON לא תקין: ' + e.message);
2095
  }
 
2111
  jsonEditor.value = JSON.stringify(toolsData.videos, null, 2);
2112
  });
2113
 
2114
+ // Refresh buttons - Load default data from JSON files
2115
+ const refreshHandler = async () => {
2116
+ if (confirm("פעולה זו תחליף את כל הנתונים הנוכחיים (כולל שינויים שביצעת בעורך) בנתוני ברירת המחדל. האם להמשיך?")) {
2117
+ console.log("Refreshing data from default JSON files...");
2118
+ toolsData = await fetchDefaultData(); // Fetch the defaults
2119
+ saveData(); // Overwrite localStorage with defaults
2120
+ renderTools();
2121
+ renderVideos();
2122
+ updateStats();
2123
+ // Reset filters and search
2124
+ searchInput.value = '';
2125
+ currentSearchTerm = '';
2126
+ filterButtons.forEach(btn => btn.classList.remove('active'));
2127
+ const allFilterBtn = document.querySelector('.filter-btn[data-category="all"]');
2128
+ if (allFilterBtn) {
2129
+ allFilterBtn.classList.add('active');
2130
+ }
2131
+ currentCategory = 'all';
2132
+ renderTools(); // Render again with reset filters
2133
+ alert("הנתונים רועננו לערכי ברירת המחדל.");
2134
+ }
2135
+ };
2136
 
2137
+ refreshBtn.addEventListener('click', refreshHandler);
2138
+ refreshBtnMobile.addEventListener('click', refreshHandler);
 
 
 
 
 
2139
  }
2140
 
2141
+ // Initialize the app when the DOM is ready
2142
  document.addEventListener('DOMContentLoaded', init);
2143
+
2144
+ // --- Keep the separate getYouTubeID and renderVideos for DOMContentLoaded if needed ---
2145
+ // Although the main init() now calls renderVideos after data loading,
2146
+ // keeping this might be a fallback or part of the original structure you wanted.
2147
+ // It's slightly redundant now if init() works correctly.
2148
+ /*
2149
+ function getYouTubeID(url) { // Already defined above, potentially remove this duplicate
2150
+ const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([^?&\/\s]+)/);
2151
+ return match ? match[1] : '';
2152
+ }
2153
+
2154
+ function renderVideos() { // Already defined above, potentially remove this duplicate
2155
+ // ... (implementation is identical to the one inside the main script block) ...
2156
+ }
2157
+
2158
+ window.addEventListener('DOMContentLoaded', () => {
2159
+ // This might run before init() finishes loading data if init is slow.
2160
+ // It's safer to rely on init() calling renderVideos.
2161
+ // renderVideos();
2162
+ });
2163
+ */
2164
+
2165
  </script>
2166
+ </body>
2167
  </html>