Spaces:
Running
Running
update to read fron .json files
Browse files- 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 =
|
1666 |
|
1667 |
-
// Initialize
|
1668 |
-
function init() {
|
1669 |
-
loadData();
|
1670 |
renderTools();
|
1671 |
-
renderVideos();
|
1672 |
updateStats();
|
1673 |
setupEventListeners();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1674 |
}
|
1675 |
|
1676 |
-
// Load data from localStorage or
|
1677 |
-
function loadData() {
|
1678 |
const savedData = localStorage.getItem('aiToolsData');
|
1679 |
if (savedData) {
|
1680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1681 |
} else {
|
1682 |
-
toolsData =
|
1683 |
-
saveData();
|
1684 |
}
|
1685 |
}
|
1686 |
|
1687 |
// Save data to localStorage
|
1688 |
function saveData() {
|
1689 |
-
|
|
|
|
|
|
|
|
|
|
|
1690 |
}
|
1691 |
|
1692 |
-
// Render tools based on filters
|
1693 |
function renderTools() {
|
|
|
|
|
|
|
|
|
1694 |
const filteredTools = filterTools();
|
1695 |
-
|
1696 |
-
toolsContainer.innerHTML = '';
|
1697 |
-
|
1698 |
-
filteredTools.
|
1699 |
-
|
1700 |
-
|
1701 |
-
|
1702 |
-
|
1703 |
-
|
1704 |
-
|
1705 |
-
|
1706 |
-
|
1707 |
-
<div>
|
1708 |
-
<
|
1709 |
-
|
|
|
|
|
|
|
|
|
|
|
1710 |
</div>
|
1711 |
-
|
1712 |
-
|
1713 |
-
|
1714 |
-
|
1715 |
-
|
|
|
1716 |
</div>
|
1717 |
-
${tool.
|
1718 |
-
|
1719 |
-
|
1720 |
-
|
1721 |
-
|
1722 |
-
|
1723 |
-
|
1724 |
-
|
1725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1726 |
}
|
1727 |
|
1728 |
-
// Render videos
|
1729 |
function renderVideos() {
|
1730 |
-
videosContainer.
|
1731 |
-
|
1732 |
-
|
1733 |
-
|
1734 |
-
|
1735 |
-
|
1736 |
-
|
1737 |
-
|
1738 |
-
|
1739 |
-
|
1740 |
-
|
1741 |
-
|
1742 |
-
|
1743 |
-
|
1744 |
-
|
1745 |
-
|
1746 |
-
|
1747 |
-
|
1748 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1749 |
}
|
1750 |
|
1751 |
-
// Filter tools
|
1752 |
function filterTools() {
|
|
|
|
|
|
|
1753 |
return toolsData.tools.filter(tool => {
|
1754 |
-
const
|
1755 |
-
const
|
1756 |
-
|
1757 |
-
|
|
|
|
|
|
|
|
|
1758 |
});
|
1759 |
}
|
1760 |
|
1761 |
-
// Update statistics
|
1762 |
function updateStats() {
|
1763 |
-
|
1764 |
-
|
1765 |
-
|
|
|
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 |
-
|
1778 |
-
|
1779 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
1783 |
-
|
1784 |
-
|
1785 |
-
|
|
|
1786 |
}
|
1787 |
|
1788 |
-
// Get category name
|
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 |
-
|
1827 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1828 |
}
|
1829 |
|
1830 |
-
|
|
|
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 |
-
|
1855 |
-
|
|
|
1856 |
jsonEditorModal.classList.remove('hidden');
|
1857 |
-
|
1858 |
|
1859 |
-
|
1860 |
-
|
1861 |
-
|
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 |
-
|
1908 |
-
|
1909 |
-
|
1910 |
-
|
1911 |
-
|
1912 |
-
|
1913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1914 |
|
1915 |
-
|
1916 |
-
|
1917 |
-
saveData();
|
1918 |
-
renderTools();
|
1919 |
-
renderVideos();
|
1920 |
-
updateStats();
|
1921 |
-
});
|
1922 |
}
|
1923 |
|
1924 |
-
// Initialize the app
|
1925 |
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1926 |
</script>
|
1927 |
-
|
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>
|