Spaces:
Running
Running
add new sort
Browse files- 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
|
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 |
-
|
|
|
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 |
-
|
|
|
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 |
-
|
|
|
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">עריכת
|
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 |
-
©
|
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 |
-
|
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();
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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">לא נמצאו כלים התואמים את
|
561 |
} else {
|
562 |
filteredTools.forEach(tool => {
|
563 |
const toolCard = document.createElement('div');
|
564 |
-
//
|
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
|
571 |
</div>
|
572 |
<div class="flex-grow">
|
573 |
-
<h3 class="text-xl font-semibold">${tool.name
|
574 |
-
<span class="text-xs px-2 py-1 rounded-full ${getCategoryBadgeColor(tool.category)}">${getCategoryName(tool.category
|
575 |
</div>
|
|
|
576 |
</div>
|
577 |
-
<p class="text-gray-700 mb-4 text-sm min-h-[60px]">${tool.description
|
578 |
<div class="flex justify-between items-center mb-4">
|
579 |
<div class="flex">
|
580 |
-
${renderRatingStars(tool.rating
|
581 |
</div>
|
582 |
-
|
583 |
</div>
|
584 |
-
<a href="${tool.url
|
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
|
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
|
602 |
function renderVideos() {
|
603 |
if (!videosContainer || !toolsData || !Array.isArray(toolsData.videos)) {
|
604 |
console.error("Cannot render videos: Missing container or invalid data.");
|
605 |
-
return;
|
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 |
-
|
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
|
626 |
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
627 |
allowfullscreen></iframe>
|
628 |
` : `
|
629 |
-
<p class="text-gray-500 text-sm"
|
630 |
`}
|
631 |
</div>
|
632 |
<div class="p-4">
|
633 |
-
<h3 class="text-lg font-semibold mb-2">${video.title
|
634 |
-
<p class="text-gray-600 text-sm mb-3 min-h-[40px]">${video.description
|
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 (
|
645 |
function filterTools() {
|
646 |
if (!toolsData || !Array.isArray(toolsData.tools)) {
|
647 |
-
return [];
|
648 |
}
|
649 |
return toolsData.tools.filter(tool => {
|
650 |
-
const
|
651 |
-
const
|
652 |
-
const
|
653 |
-
|
|
|
|
|
654 |
|
655 |
const matchesCategoryFilter = currentCategory === 'all' || tool.category === currentCategory;
|
656 |
|
@@ -658,7 +708,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
658 |
});
|
659 |
}
|
660 |
|
661 |
-
// Update statistics
|
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
|
677 |
function renderRatingStars(rating) {
|
678 |
let stars = '';
|
679 |
-
const filledStars = Math.max(0, Math.min(5, Math.round(rating
|
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>'; //
|
685 |
}
|
686 |
}
|
687 |
return stars;
|
688 |
}
|
689 |
|
690 |
-
// Get category name
|
691 |
function getCategoryName(category) {
|
692 |
const categories = {
|
693 |
-
'productivity': 'פרודוקטיביות',
|
694 |
-
'
|
695 |
-
'
|
696 |
-
'
|
697 |
-
'
|
698 |
-
'
|
699 |
-
'
|
700 |
-
'
|
701 |
-
'
|
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 || 'כללי';
|
719 |
}
|
720 |
|
721 |
-
// Get category color
|
722 |
function getCategoryColor(category) {
|
723 |
const colors = {
|
724 |
-
'productivity': 'bg-blue-600',
|
725 |
-
'
|
726 |
-
'
|
727 |
-
'
|
728 |
-
'
|
729 |
-
'
|
730 |
-
'
|
731 |
-
'
|
732 |
-
'
|
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';
|
750 |
}
|
751 |
|
752 |
-
// Get category badge color
|
753 |
function getCategoryBadgeColor(category) {
|
754 |
const colors = {
|
755 |
-
'productivity': 'bg-blue-100 text-blue-800',
|
756 |
-
'
|
757 |
-
'
|
758 |
-
'
|
759 |
-
'
|
760 |
-
'
|
761 |
-
'
|
762 |
-
'
|
763 |
-
|
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';
|
781 |
}
|
782 |
|
783 |
-
// Format date
|
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
|
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 (
|
826 |
const openEditorHandler = () => {
|
827 |
-
|
828 |
-
|
829 |
-
jsonEditorModal.classList.remove('hidden');
|
830 |
};
|
831 |
|
832 |
-
//
|
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 |
-
|
840 |
-
});
|
841 |
-
|
842 |
-
cancelEditBtn.addEventListener('click', () => {
|
843 |
-
jsonEditorModal.classList.add('hidden');
|
844 |
-
});
|
845 |
|
846 |
saveJsonBtn.addEventListener('click', () => {
|
847 |
try {
|
848 |
-
const
|
|
|
|
|
849 |
if (showToolsBtn.classList.contains('bg-blue-600')) {
|
850 |
-
|
851 |
-
toolsData.tools =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
852 |
} else {
|
853 |
-
|
854 |
-
toolsData.videos =
|
|
|
|
|
|
|
|
|
|
|
855 |
}
|
856 |
-
saveData();
|
857 |
renderTools();
|
858 |
-
renderVideos();
|
859 |
updateStats();
|
860 |
jsonEditorModal.classList.add('hidden');
|
861 |
alert("הנתונים נשמרו בהצלחה (באחסון המקומי).");
|
862 |
} catch (e) {
|
863 |
-
alert('JSON לא
|
|
|
864 |
}
|
865 |
});
|
866 |
|
@@ -880,26 +900,52 @@ window.addEventListener('DOMContentLoaded', () => {
|
|
880 |
jsonEditor.value = JSON.stringify(toolsData.videos, null, 2);
|
881 |
});
|
882 |
|
883 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
884 |
const refreshHandler = async () => {
|
885 |
-
if (confirm("פעולה זו תחליף את כל הנתונים הנוכחיים (כולל שינויים שביצעת בעורך) בנתוני ברירת
|
886 |
console.log("Refreshing data from default JSON files...");
|
887 |
-
|
888 |
-
|
889 |
-
|
890 |
-
|
891 |
-
|
892 |
-
|
893 |
-
|
894 |
-
|
895 |
-
|
896 |
-
|
897 |
-
|
898 |
-
|
899 |
-
|
900 |
-
|
901 |
-
|
902 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|