• 1 הצבעות
    10 פוסטים
    15 צפיות
    C
    @הדיבל-המעופף כתב: אולי תעשה תיקייה בדרייב עם כל ההודעות מערכת של המודל הוספתי בסוף הפוסט, לאחר הרשימה של מספרי ההודעות
  • 0 הצבעות
    4 פוסטים
    38 צפיות
    ה
    @תן-חיוך-1 בAPI המזהה הוא כזה sayInfoOnAnswer=true כמו שאפשר לראות פה
  • הקלטת הודעה ותגובה

    עזרה הדדית למשתמשים מתקדמים
    3
    0 הצבעות
    3 פוסטים
    16 צפיות
    א
    @המומחה יש את ההגדרות עיין בהגדרות ש ל השמעת קבצים (באפשריות מקשים)
  • הקו לא עונה לי -מה יכולה להיות הבעיה..

    על הפורום
    18
    0 הצבעות
    18 פוסטים
    66 צפיות
    ה
    @יהודית-ברטלר @שמואל רוצה לעזור?
  • 1 הצבעות
    1 פוסטים
    10 צפיות
    אין תגובות
  • 8 הצבעות
    34 פוסטים
    220 צפיות
    ה
    @מייבין-במקצת תגיד למערכת שלך, מה שכתוב בספוילר עולה?
  • 0 הצבעות
    34 פוסטים
    226 צפיות
    ה
    @מ.ל.ל גירסא חדשה למתמחים טופ (להוראות עיין בפוסט הראשון) קוד הסקריפט /** * מתמחים טופ - פרוקסי לפורום (NodeBB) עבור ממשק ה-HTML המתקדם. * (מבוסס על אותו קוד שנבנה עבור פורום FreeIVR - אותה תוכנת פורום, NodeBB) * * שימו לב: בפורום זה חיפוש דורש התחברות ("התחברו או הירשמו כדי לחפש" * מופיע באתר) - לכן ה-API של החיפוש עלול להחזיר שגיאה או תוצאות ריקות * למשתמשים אנונימיים. הלקוח (HTML) כבר כולל נפילה חזרה לחיפוש גוגל * בתוך הפורום למקרה כזה. * * שינויים עיקריים לעומת גרסה בסיסית: * - תמיכה אמיתית ב-pid (קישור ישיר לפוסט בודד / ציטוטים). * - ניסיונות חוזרים (retry) עם השהיה קצרה כשהשרת החיצוני נכשל באופן זמני. * - נירמול אחיד לתוצאות "פוסטים אחרונים" ו"חיפוש". * - הודעות שגיאה עקביות בצורה {error:true, message:"..."} . * - מצב דיבוג (debug=1) לפתרון תקלות בפתרון פוסט בודד. */ var BASE_URL = 'https://mitmachim.top'; var FETCH_OPTIONS = { muteHttpExceptions: true, followRedirects: false, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } }; function doGet(e) { try { var params = (e && e.parameter) || {}; var tid = params.tid || ''; var cid = params.cid || ''; var pid = params.pid || ''; var page = params.page || ''; var debug = !!(params.debug); var recent = !!(params.recent || params.action === 'recent' || params.q === 'recent' || params.sort === 'recent'); var term = params.term || params.q || params.search || params.query || ''; var cache = CacheService.getScriptCache(); if (pid) { return handlePostRedirect(pid, cache, debug); } if (tid) { return respondWithCache( BASE_URL + '/api/topic/' + encodeURIComponent(tid) + (page ? '?page=' + encodeURIComponent(page) : ''), 'topic_' + tid + '_p' + (page || 1), cache, false ); } if (cid) { return respondWithCache( BASE_URL + '/api/category/' + encodeURIComponent(cid) + (page ? '?page=' + encodeURIComponent(page) : ''), 'category_' + cid + '_p' + (page || 1), cache, false ); } if (recent) { return respondWithCache( BASE_URL + '/api/recent' + (page ? '?page=' + encodeURIComponent(page) : ''), 'recent_p' + (page || 1), cache, true ); } if (term) { return respondWithCache( BASE_URL + '/api/search?term=' + encodeURIComponent(term) + (page ? '&page=' + encodeURIComponent(page) : ''), 'search_' + term + '_p' + (page || 1), cache, true ); } // ברירת מחדל: דף הבית (רשימת קטגוריות) return respondWithCache(BASE_URL + '/api/', 'home_p1', cache, false); } catch (err) { return jsonOutput({ error: true, message: 'שגיאה כללית בשרת: ' + err.message }); } } /** * שולף מהמטמון אם קיים, אחרת שולף מהפורום, מנרמל אם צריך, ושומר במטמון. */ function respondWithCache(url, cacheKey, cache, normalize) { var cached = safeCacheGet(cache, cacheKey); if (cached) return jsonOutput(cached); var data = fetchJsonWithRetry(url); if (data && data.error) { return jsonOutput(data); } if (normalize) { data = { items: normalizeListItems(data) }; } safeCachePut(cache, cacheKey, data); return jsonOutput(data); } /** * מטפל בקישור ישיר לפוסט בודד (pid) - למשל ציטוטים או לינקים ל-/post/12345. * NodeBB לא מחזיק דף JSON נפרד לפוסט בודד; הוא מפנה (redirect) לעמוד המתאים * בתוך הנושא. אנחנו מנסים כמה דרכים כדי לתמוך בהתנהגויות אפשריות שונות: * 1. קריאה תחת /api/post/:pid - לפעמים מחזיר ישר את נתוני הנושא, ולפעמים * מחזיר תשובה עם path של הפניה. * 2. אם זה לא עבד - קריאה ל-/post/:pid הרגיל וקריאת כותרת ה-Location * מההפניה (301/302) ישירות, בלי לעקוב אחריה אוטומטית. * בסיום, טוענים את נתוני הנושא המלאים ומצרפים highlightPid כדי שהלקוח * יוכל לגלול/להדגיש את הפוסט הרלוונטי. */ function handlePostRedirect(pid, cache, debug) { var cacheKey = 'post_' + pid; if (!debug) { var cached = safeCacheGet(cache, cacheKey); if (cached) return jsonOutput(cached); } var tid = ''; var page = 1; var targetPath = ''; var trace = []; try { var res = UrlFetchApp.fetch(BASE_URL + '/api/post/' + encodeURIComponent(pid), FETCH_OPTIONS); var code = res.getResponseCode(); if (code === 200) { var bodyText = res.getContentText(); var parsed = safeJsonParse(bodyText); if (parsed && Array.isArray(parsed.posts)) { // כבר קיבלנו את כל נתוני הנושא ישירות trace.push('נסיון 1 (/api/post/): קיבלנו JSON מלא של הנושא ישירות'); parsed.highlightPid = pid; if (debug) parsed._debugTrace = trace; safeCachePut(cache, cacheKey, parsed); return jsonOutput(parsed); } if (parsed && parsed.tid) { tid = String(parsed.tid); page = parsed.page || 1; trace.push('נסיון 1 (/api/post/): קיבלנו JSON עם tid=' + tid); } else if (parsed && parsed.path) { targetPath = parsed.path; trace.push('נסיון 1 (/api/post/): קיבלנו JSON עם path=' + targetPath); } else { trace.push('נסיון 1 (/api/post/): HTTP 200 אך תשובה לא מזוהה: ' + bodyText.substring(0, 200)); } } else if (code >= 300 && code < 400) { targetPath = getLocationHeader(res); trace.push('נסיון 1 (/api/post/): הפניה (HTTP ' + code + ') אל ' + targetPath); } else { trace.push('נסיון 1 (/api/post/): HTTP ' + code + ', תחילת גוף התשובה: ' + res.getContentText().substring(0, 200)); } } catch (e1) { trace.push('נסיון 1 (/api/post/) נכשל עם שגיאה: ' + e1.message); } if (!tid && !targetPath) { try { var res2 = UrlFetchApp.fetch(BASE_URL + '/post/' + encodeURIComponent(pid), FETCH_OPTIONS); var code2 = res2.getResponseCode(); if (code2 >= 300 && code2 < 400) { targetPath = getLocationHeader(res2); trace.push('נסיון 2 (/post/): הפניה (HTTP ' + code2 + ') אל ' + targetPath); } else { trace.push('נסיון 2 (/post/): HTTP ' + code2 + ' (לא הפניה), תחילת גוף התשובה: ' + res2.getContentText().substring(0, 200)); } } catch (e2) { trace.push('נסיון 2 (/post/) נכשל עם שגיאה: ' + e2.message); } } if (!tid && targetPath) { var tidMatch = targetPath.match(/topic\/(\d+)/); if (tidMatch) tid = tidMatch[1]; var pageMatch = targetPath.match(/[?&]page=(\d+)/); if (pageMatch) page = parseInt(pageMatch[1], 10); } if (!tid) { var errObj = { error: true, message: 'לא נמצא נושא מתאים עבור הפוסט המבוקש (ID: ' + pid + ')' }; if (debug) errObj._debugTrace = trace; return jsonOutput(errObj); } var topicUrl = BASE_URL + '/api/topic/' + tid + (page > 1 ? '?page=' + page : ''); var data = fetchJsonWithRetry(topicUrl); if (data && !data.error) { data.highlightPid = pid; if (debug) data._debugTrace = trace; safeCachePut(cache, cacheKey, data); } else if (debug && data) { data._debugTrace = trace; } return jsonOutput(data); } function getLocationHeader(response) { var headers = response.getAllHeaders() || {}; for (var key in headers) { if (key.toLowerCase() === 'location') { var val = headers[key]; return Array.isArray(val) ? val[0] : val; } } return ''; } /** * שליפת JSON עם עד 3 ניסיונות והשהיה קצרה ביניהם, כדי לצמצם כשלים זמניים * (טיימאאוט / שגיאת שרת חולפת) שגורמים לתחושת "לפעמים זה נכשל". */ function fetchJsonWithRetry(url) { var attempts = 3; var lastMessage = 'שגיאה לא ידועה'; for (var i = 0; i < attempts; i++) { try { var response = UrlFetchApp.fetch(url, FETCH_OPTIONS); var code = response.getResponseCode(); if (code === 200) { var parsed = safeJsonParse(response.getContentText()); if (parsed) return parsed; lastMessage = 'הפורום החזיר תשובה שאינה JSON תקין'; } else if (code === 404) { return { error: true, message: 'התוכן המבוקש לא נמצא בפורום (404)' }; } else if (code === 301 || code === 302) { lastMessage = 'הפורום ביקש הפניה לא צפויה (HTTP ' + code + ')'; } else { lastMessage = 'שגיאת שרת חיצוני (HTTP ' + code + ')'; } } catch (err) { lastMessage = 'שגיאת רשת: ' + err.message; } if (i < attempts - 1) { Utilities.sleep(350 * (i + 1)); } } return { error: true, message: lastMessage }; } /** * ל-NodeBB יש שתי צורות עיקריות להחזרת רשימות: * - { topics: [ {tid, title, viewcount, postcount, ...} ] } (למשל קטגוריה) * - { posts: [ {tid, content, topic:{tid,title,slug}, user, ...} ] } (למשל חיפוש) * הפונקציה הזו הופכת את שתיהן לרשימה אחידה אחת כדי שהלקוח לא יצטרך לנחש. */ function normalizeListItems(raw) { var list = []; if (!raw) return list; if (Array.isArray(raw)) { list = raw; } else if (Array.isArray(raw.topics)) { list = raw.topics; } else if (Array.isArray(raw.posts)) { list = raw.posts; } var result = []; for (var i = 0; i < list.length; i++) { var item = list[i] || {}; var topic = item.topic || item; var user = item.user || (item.teaser && item.teaser.user) || null; var tid = item.tid || topic.tid; if (!tid) continue; result.push({ tid: String(tid), title: topic.title || item.title || 'נושא ללא כותרת', cid: item.cid || topic.cid || '', viewcount: (topic.viewcount !== undefined ? topic.viewcount : item.viewcount) || 0, postcount: (topic.postcount !== undefined ? topic.postcount : item.postcount) || 0, snippet: (item.content || '').toString().slice(0, 240), authorName: user ? (user.displayname || user.username || '') : '', timestampISO: item.timestampISO || item.timestamp || '' }); } return result; } function safeJsonParse(text) { try { return JSON.parse(text); } catch (e) { return null; } } function safeCacheGet(cache, key) { try { var val = cache.get(key); return val ? safeJsonParse(val) : null; } catch (e) { return null; } } function safeCachePut(cache, key, data) { try { var text = JSON.stringify(data); if (text.length < 90000) { cache.put(key, text, 180); } } catch (e) { // חורג ממגבלת הגודל של המטמון - פשוט לא שומרים, לא קריטי } } function jsonOutput(obj) { if (obj && typeof obj === 'object') { obj._backendVersion = 'v3-2026-08-10-pid-debug'; } return ContentService.createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } קוד הדף <!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>מתמחים טופ - ממשק מתקדם</title> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@500;700;800;900&family=Assistant:wght@400;500;600;700&display=swap" rel="stylesheet"> <style> :root { --bg-color: #faf9f7; --text-color: #1c1917; --card-bg: #ffffff; --border-color: #e7e5e4; --accent: #d97706; --accent-dark: #b45309; --primary-900: #042f2e; --primary-800: #0f4c4a; --primary-700: #0e5f5c; } body { font-family: 'Assistant', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.25s ease, color 0.25s ease; } h1, h2, h3, h4, .font-display { font-family: 'Rubik', 'Assistant', system-ui, sans-serif; } pre, code { font-family: Consolas, Monaco, 'Courier New', Courier, monospace; direction: ltr; text-align: left; } pre { background-color: #0c1a1a; color: #e2f4f1; border-radius: 12px; padding: 20px; padding-top: 45px; overflow-x: auto; margin: 16px 0; font-size: 0.92rem; position: relative; box-shadow: inset 0 2px 4px rgba(0,0,0,0.25); } code { background-color: #f0efeb; border: 1px solid #e7e5e4; padding: 2px 6px; border-radius: 6px; font-size: 0.88rem; color: #0c1a1a; } pre code { background-color: transparent; border: none; padding: 0; color: inherit; } .post-content { word-wrap: break-word; overflow-wrap: break-word; } .post-content a { color: var(--accent-dark); text-decoration: underline; text-underline-offset: 2px; font-weight: 600; } .post-content a:hover { color: var(--accent); } .post-content img { max-width: 100%; border-radius: 10px; margin: 8px 0; } .post-content blockquote { border-right: 3px solid var(--accent); padding: 4px 14px; margin: 12px 0; background: rgba(217, 119, 6, 0.06); border-radius: 6px; color: inherit; } html { scroll-behavior: smooth; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } } /* --- Spoiler / details enhancement (NodeBB spoiler plugin support) --- */ .post-content details { border: 1px dashed #a8a29e; border-radius: 10px; padding: 10px 14px; margin: 12px 0; background: rgba(0,0,0,0.02); } .post-content details summary { cursor: pointer; font-weight: 700; color: var(--accent-dark); list-style: none; display: flex; align-items: center; gap: 6px; } .post-content details summary::before { content: '👁️'; font-size: 0.85em; } .post-content details summary::-webkit-details-marker { display: none; } .post-content details[open] summary::after { content: ' (לחץ להסתרה)'; font-weight: 400; font-size: 0.75em; color: #78716c; } .post-content details:not([open]) summary::after { content: ' (לחץ לחשיפה)'; font-weight: 400; font-size: 0.75em; color: #78716c; } .spoiler-fallback { cursor: pointer; border: 1px dashed #a8a29e; border-radius: 10px; padding: 10px 14px; margin: 12px 0; background: rgba(0,0,0,0.02); position: relative; } .spoiler-fallback:not(.revealed) > * { filter: blur(6px); user-select: none; } .spoiler-fallback:not(.revealed)::after { content: '👁️ לחץ לחשיפת התוכן המוסתר'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--accent-dark); background: rgba(250,249,247,0.85); border-radius: 10px; } body.dark-mode .spoiler-fallback:not(.revealed)::after { background: rgba(28,25,23,0.85); } /* --- Skeleton loading shimmer --- */ .skeleton { background: linear-gradient(100deg, #eceae6 30%, #f6f5f2 45%, #eceae6 60%); background-size: 200% 100%; animation: shimmer 1.4s ease-in-out infinite; border-radius: 12px; } @keyframes shimmer { 0% { background-position: 150% 0; } 100% { background-position: -50% 0; } } body.dark-mode .skeleton { background: linear-gradient(100deg, #1f2937 30%, #263244 45%, #1f2937 60%); background-size: 200% 100%; } /* --- New reply badge for tracked topics --- */ .new-badge { background: var(--accent); color: white; font-weight: 800; border-radius: 999px; padding: 2px 9px; font-size: 0.72rem; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } /* Dark Mode Variables & Overrides */ body.dark-mode { --bg-color: #14181a; --text-color: #f1f5f4; --card-bg: #1e2528; --border-color: #313b3e; } body.dark-mode .bg-white { background-color: #1e2528 !important; color: #f1f5f4 !important; border-color: #313b3e !important; } body.dark-mode .text-stone-800, body.dark-mode .text-stone-900, body.dark-mode .text-stone-700 { color: #f1f5f4 !important; } body.dark-mode .text-stone-500, body.dark-mode .text-stone-400 { color: #a3adae !important; } body.dark-mode .border-stone-200, body.dark-mode .border-stone-100 { border-color: #313b3e !important; } body.dark-mode .bg-stone-50, body.dark-mode .bg-stone-100 { background-color: #181f21 !important; color: #cbd5d4 !important; } body.dark-mode .divide-stone-100 > * + * { border-color: #313b3e !important; } /* Print & PDF Export Clean Styles */ @media print { header, footer, button, .no-print, #tabs-bar { display: none !important; } body { background: white !important; color: black !important; } main { max-width: 100% !important; padding: 0 !important; margin: 0 !important; } .bg-white { box-shadow: none !important; border: none !important; padding: 0 !important; margin-bottom: 20px !important; } } </style> </head> <body class="min-h-screen flex flex-col justify-between"> <div> <!-- Header --> <header class="bg-gradient-to-l from-teal-950 via-teal-900 to-cyan-950 text-white shadow-xl py-6 px-6 mb-8 border-b border-teal-800/60"> <div class="max-w-5xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4"> <div class="flex items-center gap-3 cursor-pointer" onclick="loadHome()"> <div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-amber-500 to-orange-400 flex items-center justify-center shadow-lg font-display font-bold text-xl text-teal-950"> מ </div> <div> <h1 class="text-2xl font-display font-extrabold tracking-tight hover:text-amber-300 transition"> מתמחים טופ </h1> <p class="text-xs text-teal-200/80 font-medium">ממשק גלישה מתקדם ומהיר · פורום הטכנולוגיה</p> </div> </div> <!-- Top Quick Actions --> <div class="flex flex-wrap items-center gap-2"> <button onclick="loadHome()" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 px-3.5 py-2 rounded-xl text-xs font-semibold transition backdrop-blur-md cursor-pointer flex items-center gap-1.5"> 🏠 הבית </button> <button onclick="loadRecentPosts()" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 px-3.5 py-2 rounded-xl text-xs font-semibold transition backdrop-blur-md cursor-pointer flex items-center gap-1.5"> 🔥 אחרונים </button> <button onclick="loadBookmarks()" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 px-3.5 py-2 rounded-xl text-xs font-semibold transition backdrop-blur-md cursor-pointer flex items-center gap-1.5"> ⭐ מועדפים (<span id="bm-count">0</span>) </button> <button onclick="loadTracking()" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 px-3.5 py-2 rounded-xl text-xs font-semibold transition backdrop-blur-md cursor-pointer flex items-center gap-1.5"> 📡 מעקב (<span id="tr-count">0</span>) </button> <button onclick="loadHistory()" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 px-3.5 py-2 rounded-xl text-xs font-semibold transition backdrop-blur-md cursor-pointer flex items-center gap-1.5"> 📜 היסטוריה </button> <button onclick="toggleDarkMode()" id="dark-mode-btn" class="bg-white/10 hover:bg-white/20 text-white border border-white/20 p-2 rounded-xl text-xs transition backdrop-blur-md cursor-pointer" title="מצב לילה / בהיר"> 🌙 </button> <div class="flex items-center bg-white/10 border border-white/20 rounded-xl overflow-hidden backdrop-blur-md"> <button onclick="changeFontSize('decrease')" class="px-2.5 py-2 hover:bg-white/20 text-xs font-bold transition cursor-pointer" title="הקטן טקסט">A-</button> <button onclick="changeFontSize('increase')" class="px-2.5 py-2 hover:bg-white/20 text-xs font-bold transition cursor-pointer" title="הגדל טקסט">A+</button> </div> </div> </div> <!-- Advanced Controls Bar (Search & Custom URL) --> <div class="max-w-5xl mx-auto mt-6 pt-6 border-t border-teal-800/60 grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="flex items-center gap-2 bg-white/10 backdrop-blur-md p-2 rounded-2xl border border-white/20"> <input type="text" id="search-input" placeholder="חיפוש נושאים בפורום..." onkeydown="if(event.key==='Enter') handleSearch()" class="bg-transparent text-white placeholder-teal-200 px-3 py-1.5 text-sm w-full focus:outline-none"> <button onclick="handleSearch()" class="bg-white text-teal-900 px-4 py-2 rounded-xl text-xs font-bold hover:bg-teal-50 transition shrink-0 cursor-pointer shadow-sm"> 🔍 חיפוש </button> </div> <div class="flex items-center gap-2 bg-white/10 backdrop-blur-md p-2 rounded-2xl border border-white/20"> <input type="text" id="url-input" placeholder="הדבק קישור לפורום כאן (לדוגמה: /post/36372)..." onkeydown="if(event.key==='Enter') handleCustomUrl()" class="bg-transparent text-white placeholder-teal-200 px-3 py-1.5 text-sm w-full focus:outline-none" dir="ltr"> <button onclick="handleCustomUrl()" class="bg-amber-500 text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-amber-600 transition shrink-0 cursor-pointer shadow-sm"> 🔗 טען קישור </button> </div> </div> </header> <!-- Main Container --> <main class="max-w-5xl mx-auto px-4 sm:px-6 pb-16"> <div id="tabs-bar" class="hidden flex items-center gap-2 overflow-x-auto pb-3 mb-4 no-print"></div> <div id="main-container"> <div class="flex flex-col justify-center items-center py-28 gap-4"> <div class="relative w-14 h-14"> <div class="absolute inset-0 rounded-full border-4 border-teal-200 animate-pulse"></div> <div class="absolute inset-0 rounded-full border-4 border-teal-700 border-t-transparent animate-spin"></div> </div> <span class="text-stone-500 font-medium text-sm tracking-wide">טוען את נתוני המערכת...</span> </div> </div> </main> </div> <!-- Footer --> <footer class="bg-white border-t border-stone-200 py-6 text-center text-xs text-stone-400"> <p>ממשק מותאם אישית לקהילת מתמחים טופ &bull; גרסה מתקדמת</p> </footer> <script> const scriptUrl = 'כאן יש להדביק את הפריסה'; let currentCid = ''; let currentCatPage = 1; let currentFontSizeLevel = parseInt(localStorage.getItem('forum_font_size') || '16'); // Multi-Tab State Management let openTabs = []; let activeTabId = null; let currentMainView = { type: 'home', data: null }; // Infinite-scroll state for the view currently on screen let loadMoreState = null; // { kind:'topic'|'category', id, nextPage, totalPages, loading:false } let loadMoreObserver = null; document.addEventListener('DOMContentLoaded', () => { if (localStorage.getItem('forum_dark_mode') === 'true') { document.body.classList.add('dark-mode'); document.getElementById('dark-mode-btn').innerHTML = '☀️'; } updateBookmarkCount(); updateTrackingCount(); applyFontSize(); }); // --------------------------------------------------------------- // Robust fetch wrapper: timeout + automatic retry + clear errors // --------------------------------------------------------------- async function fetchJSON(url, { timeoutMs = 12000, retries = 2 } = {}) { let lastError = null; for (let attempt = 0; attempt <= retries; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timer); if (!response.ok) { throw new Error(`השרת החזיר שגיאה (HTTP ${response.status})`); } const data = await response.json(); if (data && data.error) { throw new Error(data.message || 'שגיאה בשרת'); } return data; } catch (err) { clearTimeout(timer); lastError = err.name === 'AbortError' ? new Error('תם הזמן הקצוב לבקשה - הפורום לא הגיב') : err; if (attempt < retries) { await new Promise(r => setTimeout(r, 500 * (attempt + 1))); } } } throw lastError || new Error('שגיאה לא ידועה'); } function errorBlock(message, retryFn) { return ` <div class="bg-white p-10 rounded-2xl shadow-sm text-center border border-stone-200"> <div class="text-3xl mb-3">⚠️</div> <p class="text-red-500 font-semibold mb-4">${escapeHtml(message)}</p> <button onclick="${retryFn}" class="bg-teal-800 hover:bg-teal-700 text-white px-5 py-2.5 rounded-xl text-sm font-semibold transition cursor-pointer"> 🔄 נסה שוב </button> </div> `; } function skeletonList(count = 4) { let html = '<div class="space-y-3 mb-6">'; for (let i = 0; i < count; i++) { html += `<div class="skeleton h-20 w-full"></div>`; } return html + '</div>'; } function loadingSpinner(label) { return ` <div class="flex flex-col justify-center items-center py-20 gap-4"> <div class="w-12 h-12 rounded-full border-4 border-teal-700 border-t-transparent animate-spin"></div> <span class="text-stone-500 font-medium text-sm">${escapeHtml(label)}</span> </div> `; } // --------------------------------------------------------------- // Dark mode / font size // --------------------------------------------------------------- function toggleDarkMode() { document.body.classList.toggle('dark-mode'); const isDark = document.body.classList.contains('dark-mode'); localStorage.setItem('forum_dark_mode', isDark); document.getElementById('dark-mode-btn').innerHTML = isDark ? '☀️' : '🌙'; } function changeFontSize(action) { if (action === 'increase' && currentFontSizeLevel < 22) { currentFontSizeLevel += 2; } else if (action === 'decrease' && currentFontSizeLevel > 12) { currentFontSizeLevel -= 2; } localStorage.setItem('forum_font_size', currentFontSizeLevel); applyFontSize(); } function applyFontSize() { document.querySelectorAll('.post-content').forEach(el => { el.style.fontSize = currentFontSizeLevel + 'px'; }); } // --------------------------------------------------------------- // Bookmarks // --------------------------------------------------------------- function getBookmarks() { try { return JSON.parse(localStorage.getItem('forum_bookmarks') || '[]'); } catch (e) { return []; } } function toggleBookmark(tid, title) { let bookmarks = getBookmarks(); const index = bookmarks.findIndex(b => b.tid === tid); if (index > -1) { bookmarks.splice(index, 1); } else { bookmarks.unshift({ tid, title: title || 'נושא ללא כותרת', date: new Date().toLocaleDateString('he-IL') }); } localStorage.setItem('forum_bookmarks', JSON.stringify(bookmarks)); updateBookmarkCount(); } function isBookmarked(tid) { return getBookmarks().some(b => b.tid === tid); } function updateBookmarkCount() { const badge = document.getElementById('bm-count'); if (badge) badge.innerText = getBookmarks().length; } // --------------------------------------------------------------- // Topic tracking ("מעקב נושאים") - client-side only, no login needed. // We remember the postcount we last saw; when the user reopens a // tracked topic, that becomes the new baseline (implicit "read"). // --------------------------------------------------------------- function getTracking() { try { return JSON.parse(localStorage.getItem('forum_tracking') || '[]'); } catch (e) { return []; } } function isTracked(tid) { return getTracking().some(t => t.tid === tid); } function toggleTracking(tid, title, currentPostcount) { let list = getTracking(); const idx = list.findIndex(t => t.tid === tid); if (idx > -1) { list.splice(idx, 1); } else { list.unshift({ tid, title: title || 'נושא ללא כותרת', lastPostcount: currentPostcount || 0, addedAt: new Date().toLocaleDateString('he-IL') }); } localStorage.setItem('forum_tracking', JSON.stringify(list)); updateTrackingCount(); } function markTrackedAsSeen(tid, postcount) { if (postcount === undefined || postcount === null) return; let list = getTracking(); const idx = list.findIndex(t => t.tid === tid); if (idx > -1) { list[idx].lastPostcount = postcount; localStorage.setItem('forum_tracking', JSON.stringify(list)); } } function updateTrackingCount() { const badge = document.getElementById('tr-count'); if (badge) badge.innerText = getTracking().length; } // --------------------------------------------------------------- // History // --------------------------------------------------------------- function addHistory(tid, title) { if (!tid) return; try { let history = JSON.parse(localStorage.getItem('forum_history') || '[]'); history = history.filter(h => h.tid !== tid); history.unshift({ tid, title: title || 'נושא ללא כותרת', time: new Date().toLocaleTimeString('he-IL', {hour: '2-digit', minute:'2-digit'}) }); if (history.length > 15) history.pop(); localStorage.setItem('forum_history', JSON.stringify(history)); } catch (e) {} } // --------------------------------------------------------------- // Shared render helpers // --------------------------------------------------------------- function jsAttr(str) { return String(str || '') .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '&quot;') .replace(/\n/g, ' ') .replace(/</g, '&lt;').replace(/>/g, '&gt;'); } function escapeHtml(text) { if (!text) return ''; return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); } // --------------------------------------------------------------- // NodeBB stores uploaded images / emoji / attachments under paths // that are RELATIVE to the forum's own domain (e.g. "/assets/uploads/..."). // Since this app is hosted on a different origin, relative src/href // values resolve against the wrong server and silently fail to load. // This also catches spoiler text that some NodeBB configurations // leave as raw, unrendered [spoiler]...[/spoiler] bracket syntax // instead of real HTML. // --------------------------------------------------------------- function preprocessContent(html) { if (!html) return ''; let out = String(html); out = out.replace(/(src|href|poster)="\/(?!\/)([^"]*)"/gi, '$1="https://mitmachim.top/$2"'); out = out.replace(/(src|href|poster)='\/(?!\/)([^']*)'/gi, "$1='https://mitmachim.top/$2'"); out = out.replace(/srcset="([^"]*)"/gi, (m, val) => { const fixed = val.split(',').map(part => { const seg = part.trim().split(/\s+/); if (seg[0] && seg[0].startsWith('/') && !seg[0].startsWith('//')) { seg[0] = 'https://mitmachim.top' + seg[0]; } return seg.join(' '); }).join(', '); return `srcset="${fixed}"`; }); out = out.replace(/url\((['"]?)\/(?!\/)([^)'"]*)\1\)/gi, "url($1https://mitmachim.top/$2$1)"); out = out.replace(/\[spoiler(?:=([^\]]*))?\]([\s\S]*?)\[\/spoiler\]/gi, (m, title, inner) => { const label = (title || 'Spoiler').trim(); return `<details><summary>${escapeHtml(label)}</summary><div>${inner}</div></details>`; }); return out; } function timeAgo(iso) { if (!iso) return ''; const then = new Date(iso).getTime(); if (isNaN(then)) return ''; const diffMin = Math.round((Date.now() - then) / 60000); if (diffMin < 1) return 'ממש עכשיו'; if (diffMin < 60) return `לפני ${diffMin} דק'`; const diffHr = Math.round(diffMin / 60); if (diffHr < 24) return `לפני ${diffHr} שע'`; const diffDay = Math.round(diffHr / 24); return `לפני ${diffDay} ימים`; } function topicRowHTML(t, opts = {}) { const tid = t.tid; const title = t.title || 'נושא ללא כותרת'; const cid = opts.cid !== undefined ? opts.cid : (t.cid || ''); const catPage = opts.catPage || 1; const icon = opts.icon || '💬'; const meta = []; if (t.viewcount !== undefined) meta.push(`👁️ ${t.viewcount}`); if (t.postcount !== undefined) meta.push(`💬 ${t.postcount}`); let snippetHtml = ''; if (t.snippet) { const authorPart = t.authorName ? `<strong class="text-stone-600">${escapeHtml(t.authorName)}</strong> · ` : ''; const timePart = t.timestampISO ? ` · ${timeAgo(t.timestampISO)}` : ''; snippetHtml = `<p class="text-xs text-stone-400 mt-1 line-clamp-2">${authorPart}${escapeHtml(t.snippet)}${timePart}</p>`; } return ` <div class="p-4 sm:p-5 hover:bg-teal-50/50 transition flex flex-col sm:flex-row sm:items-center justify-between gap-4 group"> <div onclick="loadTopic('${tid}', '${cid}', ${catPage}, 1, true)" class="flex items-start gap-3 flex-1 cursor-pointer min-w-0"> <div class="w-9 h-9 rounded-xl bg-stone-100 group-hover:bg-teal-800 group-hover:text-white text-stone-500 flex items-center justify-center shrink-0 transition font-bold text-xs shadow-sm"> ${icon} </div> <div class="min-w-0"> <h4 class="font-bold text-stone-800 group-hover:text-teal-800 text-base transition leading-snug">${escapeHtml(title)}</h4> ${snippetHtml} </div> </div> <div class="flex items-center gap-2 self-start sm:self-center shrink-0"> <button onclick="openInTab('${tid}', '${jsAttr(title)}', '${cid}', ${catPage}, 1)" class="bg-teal-50 hover:bg-teal-100 text-teal-800 px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer"> 📑 טאב </button> ${meta.map(m => `<span class="bg-stone-100 text-stone-600 px-3 py-1.5 rounded-lg text-xs font-medium">${m}</span>`).join('')} </div> </div> `; } // --------------------------------------------------------------- // Generic "load more" (infinite scroll + manual button + observer) // --------------------------------------------------------------- function teardownLoadMore() { if (loadMoreObserver) { loadMoreObserver.disconnect(); loadMoreObserver = null; } loadMoreState = null; } function setupLoadMore(currentPage, totalPages, isLast, loadPageFn) { teardownLoadMore(); const area = document.getElementById('load-more-area'); if (!area) return; if (isLast || currentPage >= totalPages || !totalPages) { area.innerHTML = totalPages > 1 ? `<div class="text-center text-xs text-stone-400 py-6">— הגעת לסוף הרשימה —</div>` : ''; return; } const nextPage = currentPage + 1; area.innerHTML = ` <div id="load-more-sentinel" class="text-center py-6"> <button onclick="__loadMoreClick()" class="bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-6 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer"> טען עוד (עמוד ${nextPage} מתוך ${totalPages}) ⬇️ </button> </div> `; loadMoreState = { loading: false, run: () => loadPageFn(nextPage) }; const sentinel = document.getElementById('load-more-sentinel'); if (sentinel && 'IntersectionObserver' in window) { loadMoreObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) __loadMoreClick(); }); }, { rootMargin: '500px' }); loadMoreObserver.observe(sentinel); } } function __loadMoreClick() { if (!loadMoreState || loadMoreState.loading) return; loadMoreState.loading = true; const area = document.getElementById('load-more-area'); if (area) area.innerHTML = `<div class="text-center py-6"><div class="inline-block w-6 h-6 rounded-full border-4 border-teal-700 border-t-transparent animate-spin"></div></div>`; loadMoreState.run(); } // --------------------------------------------------------------- // Home // --------------------------------------------------------------- async function loadHome() { if (scriptUrl === 'REPLACE_WITH_YOUR_APPS_SCRIPT_URL') { document.getElementById('main-container').innerHTML = ` <div class="bg-white p-10 rounded-2xl shadow-sm text-center border border-stone-200 max-w-xl mx-auto"> <div class="text-3xl mb-3">🛠️</div> <h2 class="font-display font-bold text-lg text-stone-800 mb-2">כמעט מוכן - נשאר רק חיבור לשרת</h2> <p class="text-stone-500 text-sm leading-relaxed mb-4"> הממשק הזה עדיין לא מחובר ל-Google Apps Script שלך. יש ליצור פרויקט Apps Script חדש, להדביק בו את קובץ הבק-אנד שקיבלת, לפרוס אותו כ-Web App, ואז להחליף את השורה <code class="text-xs">const scriptUrl = '...'</code> בתחילת הקוד בכתובת שקיבלת מה-Deploy. </p> </div> `; return; } currentCid = ''; activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'home' }; const container = document.getElementById('main-container'); container.innerHTML = skeletonList(6); try { const data = await fetchJSON(scriptUrl); if (data && data.categories && Array.isArray(data.categories) && data.categories.length > 0) { let html = ` <div class="mb-6"> <h2 class="text-xl font-display font-bold text-stone-800 tracking-tight">קטגוריות ראשיות</h2> <p class="text-sm text-stone-500">בחר קטגוריה לצפייה בנושאים ודיונים</p> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> `; data.categories.forEach(cat => { html += ` <div onclick="loadCategory('${cat.cid}', 1)" class="group bg-white p-5 rounded-2xl shadow-sm hover:shadow-xl border border-stone-200/80 hover:border-amber-500/60 transition-all duration-300 cursor-pointer flex flex-col justify-between gap-4 relative overflow-hidden"> <div class="absolute top-0 right-0 w-1.5 h-full bg-amber-500 opacity-0 group-hover:opacity-100 transition-opacity"></div> <div class="flex items-start justify-between gap-3"> <h3 class="font-display font-bold text-stone-800 group-hover:text-teal-800 text-lg transition line-clamp-2">${escapeHtml(cat.name)}</h3> <span class="p-2.5 rounded-xl bg-teal-50 text-teal-800 group-hover:bg-teal-800 group-hover:text-white transition-all shadow-sm">📁</span> </div> <div class="flex items-center gap-2 pt-2 border-t border-stone-100 text-xs font-medium"> <span class="bg-stone-100 text-stone-600 px-3 py-1 rounded-lg">נושאים: <strong class="text-stone-800">${cat.topic_count || 0}</strong></span> <span class="bg-teal-50/70 text-teal-800 px-3 py-1 rounded-lg">הודעות: <strong class="text-teal-900">${cat.post_count || 0}</strong></span> </div> </div> `; }); html += `</div>`; container.innerHTML = html; } else { container.innerHTML = `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-500 font-medium border border-stone-200">לא נמצאו קטגוריות להצגה.</div>`; } } catch (error) { console.error(error); container.innerHTML = errorBlock('שגיאה בטעינת דף הבית: ' + error.message, 'loadHome()'); } } // --------------------------------------------------------------- // Category (with infinite-scroll "load more" for topics) // --------------------------------------------------------------- async function loadCategory(cid, page = 1, append = false) { currentCid = cid; currentCatPage = page; if (!append) { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'category', cid, page }; } const container = document.getElementById('main-container'); if (!append) container.innerHTML = loadingSpinner('טוען נתונים מהקטגוריה...'); try { const data = await fetchJSON(`${scriptUrl}?cid=${encodeURIComponent(cid)}&page=${page}`); if (!append) { let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer flex items-center gap-2"> ← חזרה לדף הבית </button> <div class="flex items-center gap-2 bg-white border border-stone-200 p-1.5 rounded-xl shadow-sm"> <span class="text-xs text-stone-500 px-2 font-medium">קפוץ לעמוד:</span> <input type="number" id="jump-cat-page" min="1" value="${page}" class="w-16 bg-stone-50 border border-stone-200 text-center rounded-lg py-1 text-xs font-bold focus:outline-none" onkeydown="if(event.key==='Enter'){const p=parseInt(this.value);if(p>0) loadCategory('${cid}', p);}"> <button onclick="const p=parseInt(document.getElementById('jump-cat-page').value); if(p>0) loadCategory('${cid}', p);" class="bg-teal-800 text-white px-3 py-1 rounded-lg text-xs font-bold hover:bg-teal-700 transition cursor-pointer">מעבר</button> </div> </div> <div class="bg-gradient-to-br from-white to-stone-50 rounded-2xl shadow-sm border border-stone-200 p-6 sm:p-8 mb-6"> <h2 class="text-2xl font-display font-black text-stone-900 tracking-tight">${escapeHtml(data.name || 'קטגוריה')}</h2> </div> `; if (data.children && Array.isArray(data.children) && data.children.length > 0) { html += `<div class="mb-8"><h3 class="text-base font-bold text-stone-700 mb-3 px-1">תתי-קטגוריות</h3><div class="grid grid-cols-1 md:grid-cols-2 gap-3">`; data.children.forEach(sub => { html += ` <div onclick="loadCategory('${sub.cid}', 1)" class="group bg-white p-4 rounded-xl shadow-sm hover:shadow-md border border-stone-200/80 hover:border-amber-500 transition cursor-pointer flex items-center justify-between gap-3"> <span class="font-bold text-teal-900 group-hover:text-teal-700 text-sm flex-1">📁 ${escapeHtml(sub.name)}</span> <span class="bg-stone-100 text-stone-600 px-2.5 py-1 rounded-md font-medium text-xs">${sub.topic_count || 0} נושאים</span> </div> `; }); html += `</div></div>`; } const hasTopics = data.topics && Array.isArray(data.topics) && data.topics.length > 0; if (hasTopics) { html += `<h3 class="text-base font-bold text-stone-700 mb-3 px-1">נושאים בקטגוריה</h3>`; html += `<div id="topics-list" class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6"></div>`; html += `<div id="load-more-area"></div>`; } else if (!data.children || data.children.length === 0) { html += `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-400 font-medium border border-stone-200">אין נושאים או תתי-קטגוריות בקטגוריה זו.</div>`; } container.innerHTML = html; } const listEl = document.getElementById('topics-list'); if (listEl && data.topics && Array.isArray(data.topics)) { data.topics.forEach(topic => { listEl.insertAdjacentHTML('beforeend', topicRowHTML(topic, { cid, catPage: page })); }); } const totalPages = data.pageCount || (data.pagination ? data.pagination.pageCount : 1) || 1; const isLast = !!data.isLastPage || page >= totalPages; if (document.getElementById('load-more-area')) { setupLoadMore(page, totalPages, isLast, (nextPage) => loadCategory(cid, nextPage, true)); } } catch (error) { console.error(error); if (append) { const area = document.getElementById('load-more-area'); if (area) area.innerHTML = errorBlock('שגיאה בטעינת העמוד הבא: ' + error.message, `loadCategory('${cid}', ${page}, true)`); } else { container.innerHTML = errorBlock('שגיאה בטעינת הקטגוריה: ' + error.message, `loadCategory('${cid}', ${page})`); } } } // --------------------------------------------------------------- // Recent posts (normalized items[] from the backend) // --------------------------------------------------------------- async function loadRecentPosts() { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'recent' }; const container = document.getElementById('main-container'); container.innerHTML = skeletonList(6); try { const data = await fetchJSON(`${scriptUrl}?recent=true`); const items = (data && data.items) || []; let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer">← חזרה לדף הבית</button> <h2 class="text-xl font-display font-bold text-stone-800">🔥 פוסטים ודיונים אחרונים</h2> </div> `; if (items.length > 0) { html += `<div class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6">`; items.forEach(item => { html += topicRowHTML(item, { icon: '🔥' }); }); html += `</div>`; } else { html += `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-400 font-medium border border-stone-200">לא נמצאו פוסטים אחרונים כרגע.</div>`; } container.innerHTML = html; } catch (error) { console.error(error); container.innerHTML = errorBlock('שגיאה בטעינת פוסטים אחרונים: ' + error.message, 'loadRecentPosts()'); } } // --------------------------------------------------------------- // Search (normalized items[] + Google site-search fallback) // --------------------------------------------------------------- async function handleSearch() { const query = document.getElementById('search-input').value.trim(); if (!query) return; activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'search', query }; const container = document.getElementById('main-container'); container.innerHTML = loadingSpinner(`מחפש "${query}"...`); const googleFallback = `https://www.google.com/search?q=${encodeURIComponent('site:mitmachim.top ' + query)}`; try { const data = await fetchJSON(`${scriptUrl}?term=${encodeURIComponent(query)}`); const items = (data && data.items) || []; let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer">← חזרה לדף הבית</button> <h2 class="text-xl font-display font-bold text-stone-800">תוצאות חיפוש: "${escapeHtml(query)}"</h2> </div> `; if (items.length > 0) { html += `<div class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6">`; items.forEach(item => { html += topicRowHTML(item, { icon: '🔍' }); }); html += `</div>`; html += `<div class="text-center"><a href="${googleFallback}" target="_blank" rel="noopener" class="text-xs text-stone-400 hover:text-teal-700 underline">לא מוצאים את מה שחיפשתם? נסו חיפוש גוגל בתוך הפורום</a></div>`; } else { html += ` <div class="bg-white p-10 rounded-2xl shadow-sm text-center border border-stone-200"> <p class="text-stone-500 font-medium mb-4">לא נמצאו תוצאות בחיפוש הפנימי עבור "${escapeHtml(query)}".</p> <a href="${googleFallback}" target="_blank" rel="noopener" class="inline-block bg-teal-800 hover:bg-teal-700 text-white px-5 py-2.5 rounded-xl text-sm font-semibold transition">🔎 חפש בגוגל בתוך הפורום</a> </div> `; } container.innerHTML = html; } catch (error) { console.error(error); container.innerHTML = ` ${errorBlock('שגיאה בחיפוש: ' + error.message, 'handleSearch()')} <div class="text-center mt-4"> <a href="${googleFallback}" target="_blank" rel="noopener" class="inline-block bg-white hover:bg-stone-50 text-teal-800 border border-stone-200 px-5 py-2.5 rounded-xl text-sm font-semibold transition">🔎 נסה חיפוש בגוגל במקום</a> </div> `; } } // --------------------------------------------------------------- // Bookmarks // --------------------------------------------------------------- function loadBookmarks() { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'bookmarks' }; const container = document.getElementById('main-container'); const bookmarks = getBookmarks(); let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer">← חזרה לדף הבית</button> <h2 class="text-xl font-display font-bold text-stone-800">הנושאים השמורים שלי (מועדפים)</h2> </div> `; if (bookmarks.length > 0) { html += `<div class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6">`; bookmarks.forEach(bm => { html += ` <div class="p-4 sm:p-5 hover:bg-teal-50/50 transition flex flex-col sm:flex-row sm:items-center justify-between gap-4 group"> <div onclick="loadTopic('${bm.tid}', '', 1, 1, false)" class="flex items-start gap-3 flex-1 cursor-pointer"> <div class="w-9 h-9 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center shrink-0 font-bold text-xs shadow-sm">⭐</div> <div> <h4 class="font-bold text-stone-800 group-hover:text-teal-800 text-base transition leading-snug">${escapeHtml(bm.title)}</h4> <span class="text-xs text-stone-400">נשמר בתאריך: ${bm.date}</span> </div> </div> <div class="flex items-center gap-2 self-start sm:self-center"> <button onclick="openInTab('${bm.tid}', '${jsAttr(bm.title)}', '', 1, 1)" class="bg-teal-50 hover:bg-teal-100 text-teal-800 px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer">📑 פתח בטאב</button> <button onclick="toggleBookmark('${bm.tid}'); loadBookmarks();" class="bg-rose-50 hover:bg-rose-100 text-rose-600 px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer">הסר</button> </div> </div> `; }); html += `</div>`; } else { html += `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-400 font-medium border border-stone-200">אין עדיין נושאים שמורים במועדפים.</div>`; } container.innerHTML = html; } // --------------------------------------------------------------- // Topic tracking view // --------------------------------------------------------------- function loadTracking() { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'tracking' }; const container = document.getElementById('main-container'); const tracked = getTracking(); let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer">← חזרה לדף הבית</button> <div class="flex items-center gap-3"> ${tracked.length ? `<button onclick="refreshTracking()" class="bg-teal-800 hover:bg-teal-700 text-white px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer">🔄 בדוק עדכונים</button>` : ''} <h2 class="text-xl font-display font-bold text-stone-800">📡 נושאים במעקב</h2> </div> </div> `; if (tracked.length === 0) { html += `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-400 font-medium border border-stone-200">אין נושאים במעקב עדיין. פתחו נושא ולחצו על "עקוב אחר נושא" כדי להתחיל לקבל התראה על תגובות חדשות.</div>`; } else { html += `<div id="tracking-list" class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6">`; tracked.forEach(t => { html += ` <div class="p-4 sm:p-5 hover:bg-teal-50/50 transition flex flex-col sm:flex-row sm:items-center justify-between gap-4 group" id="track-row-${t.tid}"> <div onclick="loadTopic('${t.tid}', '', 1, 1, true)" class="flex items-start gap-3 flex-1 cursor-pointer"> <div class="w-9 h-9 rounded-xl bg-teal-50 text-teal-800 flex items-center justify-center shrink-0 font-bold text-xs shadow-sm">📡</div> <div> <h4 class="font-bold text-stone-800 group-hover:text-teal-800 text-base transition leading-snug">${escapeHtml(t.title)}</h4> <span class="text-xs text-stone-400">נוסף למעקב בתאריך: ${t.addedAt}</span> </div> </div> <div class="flex items-center gap-2 self-start sm:self-center"> <span class="track-status text-xs text-stone-400"></span> <button onclick="toggleTracking('${t.tid}', '${jsAttr(t.title)}'); loadTracking();" class="bg-rose-50 hover:bg-rose-100 text-rose-600 px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer">הסר ממעקב</button> </div> </div> `; }); html += `</div>`; } container.innerHTML = html; } async function refreshTracking() { const tracked = getTracking(); const results = await Promise.allSettled( tracked.map(t => fetchJSON(`${scriptUrl}?tid=${encodeURIComponent(t.tid)}&page=1`)) ); results.forEach((res, i) => { const t = tracked[i]; const statusEl = document.querySelector(`#track-row-${t.tid} .track-status`); if (!statusEl) return; if (res.status === 'fulfilled') { const val = res.value; const freshCount = val.postcount !== undefined ? val.postcount : (val.posts ? val.posts.length : t.lastPostcount); const diff = freshCount - (t.lastPostcount || 0); statusEl.innerHTML = diff > 0 ? `<span class="new-badge">+${diff} חדשות</span>` : `<span class="text-emerald-600 font-semibold text-xs">מעודכן ✓</span>`; } else { statusEl.innerHTML = `<span class="text-red-400">שגיאה בבדיקה</span>`; } }); } // --------------------------------------------------------------- // History // --------------------------------------------------------------- function loadHistory() { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'history' }; const container = document.getElementById('main-container'); let history = []; try { history = JSON.parse(localStorage.getItem('forum_history') || '[]'); } catch (e) {} let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="loadHome()" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer">← חזרה לדף הבית</button> <div class="flex items-center gap-3"> <button onclick="localStorage.removeItem('forum_history'); loadHistory();" class="bg-rose-50 hover:bg-rose-100 text-rose-600 border border-rose-200 px-3.5 py-2 rounded-xl text-xs font-bold transition cursor-pointer">🗑️ נקה היסטוריה</button> <h2 class="text-xl font-display font-bold text-stone-800">היסטוריית צפייה</h2> </div> </div> `; if (history.length > 0) { html += `<div class="bg-white rounded-2xl shadow-sm border border-stone-200 divide-y divide-stone-100 overflow-hidden mb-6">`; history.forEach(item => { html += ` <div class="p-4 sm:p-5 hover:bg-teal-50/50 transition flex flex-col sm:flex-row sm:items-center justify-between gap-4 group"> <div onclick="loadTopic('${item.tid}', '', 1, 1, false)" class="flex items-start gap-3 flex-1 cursor-pointer"> <div class="w-9 h-9 rounded-xl bg-stone-100 group-hover:bg-teal-800 group-hover:text-white text-stone-500 flex items-center justify-center shrink-0 transition font-bold text-xs shadow-sm">📜</div> <h4 class="font-bold text-stone-800 group-hover:text-teal-800 text-base transition leading-snug">${escapeHtml(item.title)}</h4> </div> <div class="flex items-center gap-2 self-start sm:self-center"> <button onclick="openInTab('${item.tid}', '${jsAttr(item.title)}', '', 1, 1)" class="bg-teal-50 hover:bg-teal-100 text-teal-800 px-3 py-1.5 rounded-lg text-xs font-bold transition cursor-pointer">📑 פתח בטאב</button> <span class="bg-stone-100 text-stone-600 px-3 py-1.5 rounded-lg text-xs font-medium">🕒 ${item.time}</span> </div> </div> `; }); html += `</div>`; } else { html += `<div class="bg-white p-12 rounded-2xl shadow-sm text-center text-stone-400 font-medium border border-stone-200">אין היסטוריית גלישה זמינה.</div>`; } container.innerHTML = html; } // --------------------------------------------------------------- // Tabs // --------------------------------------------------------------- function renderTabsBar() { const bar = document.getElementById('tabs-bar'); if (openTabs.length === 0) { bar.classList.add('hidden'); bar.innerHTML = ''; return; } bar.classList.remove('hidden'); let html = ''; openTabs.forEach(tab => { const isActive = tab.id === activeTabId; html += ` <div onclick="switchTab('${tab.id}')" class="shrink-0 flex items-center gap-2 pl-2 pr-3.5 py-2 rounded-xl border cursor-pointer transition text-xs font-bold ${isActive ? 'bg-teal-800 border-teal-800 text-white shadow-md' : 'bg-white border-stone-200 text-stone-600 hover:bg-stone-50'}"> <span class="max-w-[140px] truncate">${escapeHtml(tab.title)}</span> <span onclick="event.stopPropagation(); closeTab('${tab.id}')" class="w-4 h-4 rounded-full flex items-center justify-center ${isActive ? 'hover:bg-white/20' : 'hover:bg-stone-200'} transition">✕</span> </div> `; }); bar.innerHTML = html; } async function openInTab(tid, title, cid, catPage, postPage) { const tabId = 'tab_' + tid + '_' + Date.now(); openTabs.push({ id: tabId, tid, title: title || 'טוען...', cid, catPage, postPage: postPage || 1 }); activeTabId = tabId; renderTabsBar(); const container = document.getElementById('main-container'); teardownLoadMore(); container.innerHTML = loadingSpinner('פותח בטאב חדש...'); try { const data = await fetchJSON(`${scriptUrl}?tid=${encodeURIComponent(tid)}&page=${postPage || 1}`); const tab = openTabs.find(t => t.id === tabId); if (tab) tab.title = data.title || tab.title; if (activeTabId === tabId) { renderTabsBar(); addHistory(tid, data.title); if (isTracked(tid)) markTrackedAsSeen(tid, data.postcount !== undefined ? data.postcount : (data.posts ? data.posts.length : undefined)); renderTopicData(data, cid, catPage, postPage || 1, false, tid); } } catch (error) { console.error(error); if (activeTabId === tabId) { container.innerHTML = errorBlock('שגיאה בטעינת הטאב: ' + error.message, `openInTab('${tid}', '${jsAttr(title)}', '${cid}', ${catPage}, ${postPage || 1})`); } } } function switchTab(tabId) { const tab = openTabs.find(t => t.id === tabId); if (!tab) return; activeTabId = tabId; renderTabsBar(); loadTopic(tab.tid, tab.cid, tab.catPage, tab.postPage, false); } function closeTab(tabId) { const idx = openTabs.findIndex(t => t.id === tabId); if (idx === -1) return; const wasActive = activeTabId === tabId; openTabs.splice(idx, 1); renderTabsBar(); if (wasActive) { if (openTabs.length > 0) { switchTab(openTabs[openTabs.length - 1].id); } else { activeTabId = null; loadHome(); } } } // --------------------------------------------------------------- // Single post permalink (pid) - now properly resolved server-side // --------------------------------------------------------------- async function loadPost(pid) { activeTabId = null; teardownLoadMore(); renderTabsBar(); currentMainView = { type: 'post', pid }; const container = document.getElementById('main-container'); container.innerHTML = loadingSpinner('טוען את הפוסט המבוקש...'); try { const data = await fetchJSON(`${scriptUrl}?pid=${encodeURIComponent(pid)}`); if (data && Array.isArray(data.posts)) { const tid = String(data.tid || (data.posts[0] && data.posts[0].tid) || ''); addHistory(tid, data.title); if (isTracked(tid)) markTrackedAsSeen(tid, data.postcount); const postPage = (data.pagination && data.pagination.currentPage) || 1; renderTopicData(data, '', 1, postPage, false, tid); } else { container.innerHTML = errorBlock('לא ניתן היה לטעון את הפוסט המבוקש.', `loadPost('${pid}')`); } } catch (error) { console.error(error); container.innerHTML = errorBlock('שגיאה בטעינת הפוסט: ' + error.message, `loadPost('${pid}')`); } } // --------------------------------------------------------------- // Topic (with infinite-scroll "load more" for replies + highlight) // --------------------------------------------------------------- async function loadTopic(tid, cid, catPage, postPage = 1, openAsMain = false, append = false) { currentCid = cid; currentCatPage = catPage; if (!append) { if (openAsMain || activeTabId === null) { activeTabId = null; renderTabsBar(); } teardownLoadMore(); currentMainView = { type: 'topic', tid, cid, catPage, postPage }; } const container = document.getElementById('main-container'); if (!append) container.innerHTML = loadingSpinner('טוען תוכן הנושא...'); try { const data = await fetchJSON(`${scriptUrl}?tid=${encodeURIComponent(tid)}&page=${postPage}`); if (!append) { addHistory(tid, data.title); if (isTracked(tid)) { markTrackedAsSeen(tid, data.postcount !== undefined ? data.postcount : (data.posts ? data.posts.length : undefined)); } } renderTopicData(data, cid, catPage, postPage, append, tid); } catch (error) { console.error(error); if (append) { const area = document.getElementById('load-more-area'); if (area) area.innerHTML = errorBlock('שגיאה בטעינת העמוד הבא: ' + error.message, `loadTopic('${tid}', '${cid}', ${catPage}, ${postPage}, false, true)`); } else { container.innerHTML = errorBlock('שגיאה בטעינת הנושא: ' + error.message, `loadTopic('${tid}', '${cid}', ${catPage}, ${postPage})`); } } } function renderTopicData(data, cid, catPage, postPage, append, tid) { const container = document.getElementById('main-container'); const resolvedTid = String(data.tid || tid || ''); const bookmarked = isBookmarked(resolvedTid); const tracked = isTracked(resolvedTid); if (!append) { let html = ` <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6"> <button onclick="${cid ? `loadCategory('${cid}', ${catPage})` : 'loadHome()'}" class="self-start bg-white hover:bg-stone-50 text-stone-700 border border-stone-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer"> ← חזרה ${cid ? 'לקטגוריה' : 'לדף הבית'} </button> <div class="flex flex-wrap items-center gap-2"> <button onclick="toggleBookmark('${resolvedTid}', '${jsAttr(data.title)}'); renderTopicToolbar('${resolvedTid}', '${jsAttr(data.title)}', ${data.postcount || 0});" id="bm-toggle-btn" class="${bookmarked ? 'bg-amber-500 text-white' : 'bg-white text-stone-700 border border-stone-200'} px-3.5 py-2 rounded-xl text-xs font-bold transition shadow-sm cursor-pointer"> ${bookmarked ? '⭐ במועדפים' : '☆ הוסף למועדפים'} </button> <button onclick="toggleTracking('${resolvedTid}', '${jsAttr(data.title)}', ${data.postcount || 0}); renderTopicToolbar('${resolvedTid}', '${jsAttr(data.title)}', ${data.postcount || 0});" id="tr-toggle-btn" class="${tracked ? 'bg-teal-800 text-white' : 'bg-white text-stone-700 border border-stone-200'} px-3.5 py-2 rounded-xl text-xs font-bold transition shadow-sm cursor-pointer"> ${tracked ? '📡 עוקב אחר הנושא' : '🔔 עקוב אחר נושא'} </button> <button onclick="window.print()" class="bg-white text-stone-700 border border-stone-200 px-3.5 py-2 rounded-xl text-xs font-bold hover:bg-stone-50 transition shadow-sm cursor-pointer"> 🖨️ הדפס / PDF </button> </div> </div> <div class="bg-gradient-to-br from-white to-stone-50 rounded-2xl shadow-sm border border-stone-200 p-6 sm:p-8 mb-6"> <h2 class="text-2xl font-display font-black text-stone-900 tracking-tight leading-snug">${escapeHtml(data.title || 'נושא')}</h2> </div> <div id="posts-list" class="space-y-5 mb-6"></div> <div id="load-more-area"></div> `; container.innerHTML = html; } const listEl = document.getElementById('posts-list'); if (listEl && data.posts && Array.isArray(data.posts)) { data.posts.forEach((post, idx) => { listEl.insertAdjacentHTML('beforeend', postCardHTML(post, idx, resolvedTid)); }); // Enhance the newly-inserted post content (spoilers, code copy buttons, etc.) const newNodes = listEl.querySelectorAll('.post-content:not([data-enhanced])'); newNodes.forEach(node => enhancePostContent(node)); applyFontSize(); } const totalPages = data.pageCount || (data.pagination ? data.pagination.pageCount : 1) || 1; const isLast = !!data.isLastPage || postPage >= totalPages; if (document.getElementById('load-more-area')) { setupLoadMore(postPage, totalPages, isLast, (nextPage) => loadTopic(resolvedTid, cid, catPage, nextPage, false, true)); } // Highlight & scroll to a specific post (used for /post/:pid deep links) if (data.highlightPid && !append) { setTimeout(() => { const target = document.getElementById('post-' + data.highlightPid); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); target.classList.add('ring-2', 'ring-amber-500', 'ring-offset-2'); setTimeout(() => target.classList.remove('ring-2', 'ring-amber-500', 'ring-offset-2'), 3000); } }, 150); } } // Refresh just the bookmark/tracking toggle buttons without a full re-render function renderTopicToolbar(tid, title, postcount) { const bmBtn = document.getElementById('bm-toggle-btn'); if (bmBtn) { const bookmarked = isBookmarked(tid); bmBtn.className = `${bookmarked ? 'bg-amber-500 text-white' : 'bg-white text-stone-700 border border-stone-200'} px-3.5 py-2 rounded-xl text-xs font-bold transition shadow-sm cursor-pointer`; bmBtn.innerHTML = bookmarked ? '⭐ במועדפים' : '☆ הוסף למועדפים'; } const trBtn = document.getElementById('tr-toggle-btn'); if (trBtn) { const tracked = isTracked(tid); trBtn.className = `${tracked ? 'bg-teal-800 text-white' : 'bg-white text-stone-700 border border-stone-200'} px-3.5 py-2 rounded-xl text-xs font-bold transition shadow-sm cursor-pointer`; trBtn.innerHTML = tracked ? '📡 עוקב אחר הנושא' : '🔔 עקוב אחר נושא'; } } function postCardHTML(post, index, tid) { const user = post.user || {}; const avatarColor = user['icon:bgColor'] || '#0e5f5c'; const avatarText = user['icon:text'] || (user.username ? user.username.charAt(0) : '?'); const displayName = user.displayname || user.username || 'אנונימי'; const likesCount = (post.upvotes !== undefined) ? post.upvotes : (post.votes || 0); return ` <div id="post-${post.pid}" class="bg-white rounded-2xl shadow-sm border border-stone-200 p-5 sm:p-6 transition-shadow duration-500"> <div class="flex items-center gap-3 mb-4 pb-4 border-b border-stone-100"> <div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold shrink-0 shadow-sm" style="background-color: ${escapeHtml(avatarColor)}"> ${escapeHtml(avatarText)} </div> <div class="min-w-0"> <p class="font-bold text-stone-800 text-sm truncate">${escapeHtml(displayName)}</p> <p class="text-xs text-stone-400">${post.timestampISO ? timeAgo(post.timestampISO) : ''} ${index === 0 ? '· פותח הנושא' : ''}</p> </div> <div class="mr-auto flex items-center gap-2 shrink-0"> <span class="${likesCount > 0 ? 'bg-rose-50 text-rose-600' : 'bg-stone-100 text-stone-400'} px-2.5 py-1 rounded-lg text-xs font-bold flex items-center gap-1">❤️ ${likesCount}</span> <span class="bg-stone-100 text-stone-500 px-2.5 py-1 rounded-lg text-xs font-mono">#${index + 1}</span> </div> </div> <div class="post-content leading-relaxed text-[15px]">${preprocessContent(post.content)}</div> </div> `; } // --------------------------------------------------------------- // Post-content enhancement: spoiler reveal + copy-to-clipboard on // code blocks. Runs once per newly-inserted post. // --------------------------------------------------------------- function enhancePostContent(node) { node.setAttribute('data-enhanced', 'true'); // Copy-to-clipboard button on every code block node.querySelectorAll('pre').forEach(pre => { if (pre.querySelector('.copy-code-btn')) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'copy-code-btn absolute top-2 left-2 bg-white/10 hover:bg-white/20 text-white text-xs px-2.5 py-1 rounded-lg transition cursor-pointer'; btn.innerText = '📋 העתק'; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const codeEl = pre.querySelector('code') || pre; navigator.clipboard.writeText(codeEl.innerText).then(() => { btn.innerText = '✓ הועתק'; setTimeout(() => { btn.innerText = '📋 העתק'; }, 1500); }).catch(() => {}); }); pre.appendChild(btn); }); // Generic spoiler support. NodeBB's spoiler plugin markup varies by // version/config, so instead of hard-coding one class name, we catch // anything whose class hints at "spoiler" and make it click-to-reveal. // Native <details>/<summary> already works via the CSS above, so we // skip anything already inside one of those. // // IMPORTANT: some NodeBB spoiler plugins (e.g. nodebb-plugin-ns-spoiler // v3+) deliberately never send the hidden text to anonymous/API // consumers at all - it's only fetched live via a socket connection // when a logged-in user clicks reveal in the real NodeBB client. We // can't replicate that here, so if a spoiler turns out to be empty // after excluding its own label/button, we say so honestly instead // of just showing a blank box, and link back to the real forum. try { const postWrapper = node.closest('[id^="post-"]'); const pid = postWrapper ? postWrapper.id.replace('post-', '') : ''; node.querySelectorAll('[class*="spoiler" i]').forEach(el => { if (el.closest('details')) return; if (el.dataset.spoilerReady) return; // Skip nested elements whose own ancestor already matched - // otherwise something like nbb-spoiler-wrapper > nbb-spoiler-text // would be wrapped twice (once for the wrapper, once for its child). if (el.parentElement && el.parentElement.closest('[class*="spoiler" i]')) return; el.dataset.spoilerReady = 'true'; el.classList.add('spoiler-fallback'); const probe = el.cloneNode(true); probe.querySelectorAll('[class*="control" i], [class*="summary" i], [class*="text" i], [class*="label" i], summary, button, a').forEach(n => n.remove()); const remainingText = probe.textContent.replace(/spoiler/gi, '').trim(); const hasMedia = probe.querySelectorAll('img, video, audio, source').length > 0; const isEmpty = remainingText.length < 2 && !hasMedia; el.addEventListener('click', function handleReveal(e) { if (el.classList.contains('revealed')) return; e.preventDefault(); e.stopPropagation(); el.classList.add('revealed'); if (isEmpty) { const link = pid ? `https://mitmachim.top/post/${encodeURIComponent(pid)}` : 'https://mitmachim.top'; el.innerHTML = `<p class="text-xs text-stone-500">⚠️ הפורום לא שולח את תוכן הספוילר הזה לצפייה אנונימית/חיצונית - רק דרך הפורום המקורי (ולעיתים רק לאחר התחברות). <a href="${link}" target="_blank" rel="noopener" class="text-teal-700 font-bold underline">פתח את הפוסט בפורום המקורי ↗</a></p>`; } }); }); } catch (e) { /* querySelectorAll with the "i" flag may not be supported everywhere - fail silently */ } // Same "the forum never actually sent this" honesty check, but for // native <details>/<summary> spoilers (styled via CSS only otherwise). try { const postWrapper2 = node.closest('[id^="post-"]'); const pid2 = postWrapper2 ? postWrapper2.id.replace('post-', '') : ''; node.querySelectorAll('details').forEach(det => { if (det.dataset.spoilerChecked) return; det.dataset.spoilerChecked = 'true'; const probe = det.cloneNode(true); probe.querySelectorAll('summary').forEach(n => n.remove()); const remainingText = probe.textContent.trim(); const hasMedia = probe.querySelectorAll('img, video, audio, source').length > 0; if (remainingText.length < 2 && !hasMedia) { det.addEventListener('toggle', function () { if (det.open && !det.dataset.filledIn) { det.dataset.filledIn = 'true'; const link = pid2 ? `https://mitmachim.top/post/${encodeURIComponent(pid2)}` : 'https://mitmachim.top'; const p = document.createElement('p'); p.className = 'text-xs text-stone-500'; p.innerHTML = `⚠️ הפורום לא שולח את תוכן הספוילר הזה לצפייה חיצונית. <a href="${link}" target="_blank" rel="noopener" class="text-teal-700 font-bold underline">פתח בפורום המקורי ↗</a>`; det.appendChild(p); } }); } }); } catch (e) { /* ignore */ } } // --------------------------------------------------------------- // Custom URL loader (paste any link from the forum) // --------------------------------------------------------------- function handleCustomUrl() { const raw = document.getElementById('url-input').value.trim(); if (!raw) return; const postMatch = raw.match(/(?:^|\/)post\/(\d+)/); const tidMatch = raw.match(/(?:^|\/)topic\/(\d+)/); const cidMatch = raw.match(/(?:^|\/)category\/(\d+)/); if (postMatch) { loadPost(postMatch[1]); } else if (tidMatch) { const pageMatch = raw.match(/[?&]page=(\d+)/); const page = pageMatch ? parseInt(pageMatch[1], 10) : 1; loadTopic(tidMatch[1], '', 1, page, true); } else if (cidMatch) { loadCategory(cidMatch[1], 1); } else if (/^\d+$/.test(raw)) { loadTopic(raw, '', 1, 1, true); } else { alert('לא זוהה קישור תקין. יש להדביק קישור לנושא, לפוסט בודד או לקטגוריה מתוך הפורום.'); } document.getElementById('url-input').value = ''; } // --------------------------------------------------------------- // Internal link interception (delegated, single listener) // Handles: topic/category/post links inside post content, plus // user-mention links which now open the real forum in a new tab // instead of silently navigating the whole app away. // --------------------------------------------------------------- document.addEventListener('click', function (e) { const anchor = e.target.closest('a'); if (!anchor) return; const href = anchor.getAttribute('href'); if (!href || href.startsWith('#')) return; const postMatch = href.match(/(?:^|\/)post\/(\d+)/); const tidMatch = href.match(/(?:^|\/)topic\/(\d+)/); const cidMatch = href.match(/(?:^|\/)category\/(\d+)/); const userMatch = href.match(/(?:^|\/)(?:user|uid)\/([^\/?#]+)/); const toAbsolute = (h) => h.startsWith('http') ? h : ('https://mitmachim.top' + (h.startsWith('/') ? '' : '/') + h); if (postMatch) { e.preventDefault(); loadPost(postMatch[1]); return; } if (tidMatch) { e.preventDefault(); const pageMatch = href.match(/[?&]page=(\d+)/); const page = pageMatch ? parseInt(pageMatch[1], 10) : 1; loadTopic(tidMatch[1], currentCid, currentCatPage, page, true); return; } if (cidMatch) { e.preventDefault(); loadCategory(cidMatch[1], 1); return; } if (userMatch) { e.preventDefault(); window.open(toAbsolute(href), '_blank', 'noopener'); return; } if (href.startsWith('/') || /mitmachim\.top/.test(href)) { e.preventDefault(); window.open(toAbsolute(href), '_blank', 'noopener'); } // Anything else (external links the poster included) opens normally. }); // Kick things off loadHome(); </script> </body> </html>
  • 3 הצבעות
    55 פוסטים
    34 צפיות
    פ
    @lavitoren-הטי-ל בשבילי לא ממש... פעם היה אפשר לראות בפרופיל כמה הצבעת לאנשים אחרים והיה לי סכומים מטורפים
  • שליחת צינתוק בpost

    פורום מפתחים API
    5
    0 הצבעות
    5 פוסטים
    30 צפיות
    ה
    @CUBASE תודה, חשבתי שזה רק אם אני שולח לכמה רשימות אבל לא במקרה שאני שולח לרשימה בודדת.
  • 0 הצבעות
    11 פוסטים
    641 צפיות
    א
    סליחה שאני מקפיצה פוסט ישן... אבל משהו יודע האם מודל זימון תורים השתדרג וזמן קביעת התור קוצר? האם זה יכול לגרום לכפילויות בזימון אם משתמשים התקשרו והזמינו תוך פחות מ3 דק׳?
  • 1 הצבעות
    1 פוסטים
    24 צפיות
    אין תגובות
  • משחק ילדים מהפכני חדש בטלפון הכשר!!!👍✨

    נפתר בקשות לפיתוח
    40
    2 הצבעות
    40 פוסטים
    167 צפיות
    ה
    @lavitoren-הטי-ל וגם אני מצטרף להנ"ל
  • 0 הצבעות
    23 פוסטים
    25 צפיות
    P
    אכן @הדיבל-המעופף שלח לי מייל, כרגע אני לא צריך עוד, אולי מחר.
  • 1 הצבעות
    19 פוסטים
    118 צפיות
    ה
    @Perfect תגיד לAI מה הרעיון והוא יעשה את זה (זה לא מידי מורכב זה רק לוקח זמן)
  • תגובות

    עזרה הדדית למשתמשים מתקדמים
    12
    0 הצבעות
    12 פוסטים
    440 צפיות
    נ
    @פלפל-שחור כתב: @נועם-אלימלך אתה יכול לעזור לי בקו שלי?
  • 0 הצבעות
    6 פוסטים
    39 צפיות
    א
    @אA תודה רבה! נעזרתי בקוד שלך וסופסוף קיבלתי את הקבצים! תודה לכל מי שעזר!
  • 3 הצבעות
    83 פוסטים
    713 צפיות
    ה
    @פלוס למה?
  • 0 הצבעות
    81 פוסטים
    2k צפיות
    י
    ההודעה "יש לך הודעה אישית חדשה" לא מושמעת אצלי
  • 0 הצבעות
    267 פוסטים
    9k צפיות
    נ
    @יצחק-י. כן תודה
  • פתרונאי

    שאלות ועזרה הדדית
    2
    0 הצבעות
    2 פוסטים
    29 צפיות
    ה
    @המומחה https://ivr.pitronai.com/