@מ.ל.ל
גירסא חדשה למתמחים טופ
(להוראות עיין בפוסט הראשון)
קוד הסקריפט
/**
* מתמחים טופ - פרוקסי לפורום (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>ממשק מותאם אישית לקהילת מתמחים טופ • גרסה מתקדמת</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, '"')
.replace(/\n/g, ' ')
.replace(/</g, '<').replace(/>/g, '>');
}
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
// ---------------------------------------------------------------
// 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>