Spaces:
Running
Running
Kethan Dosapati
Enhance static UI and authentication: improve sidebar auth visibility, add mobile responsiveness, and refine error messages; update `/report` endpoint to use `ArticleOut` model; redesign styles for a Reddit-like layout with theming and responsive tweaks.
363bda3 | (function () { | |
| "use strict"; | |
| const DISPLAY_LIMIT = 20; | |
| const RECENT_TIPS_LIMIT = 5; | |
| const COMMENT_BODY_MAX = 2000; | |
| const TYPE_STYLES = { | |
| error: "background: #fef2f2; color: #b91c1c;", | |
| tip: "background: #f0fdf4; color: #15803d;", | |
| pattern: "background: #f0fdf4; color: #15803d;", | |
| guide: "background: #eff6ff; color: #1d4ed8;", | |
| reference: "background: #faf5ff; color: #7c3aed;", | |
| }; | |
| const EMPTY_STATE_HTML = ` | |
| <div class="empty-state"> | |
| <div class="empty-icon">📚</div> | |
| <div class="empty-title">Knowledge base is empty</div> | |
| <div class="empty-text">Add a tip to get started. Use Submit Tip in the sidebar.</div> | |
| </div>`; | |
| const SEARCH_EMPTY_HTML = `<div class="search-empty">Use the search bar above to find tips.</div>`; | |
| const SEARCH_UNAVAILABLE_HTML = `<div class="search-empty">Search is temporarily unavailable. Try again later.</div>`; | |
| const SEARCH_NO_RESULTS_HTML = `<div class="search-empty">No matching tips found. Try different keywords.</div>`; | |
| function formatDate(created) { | |
| if (!created) return ""; | |
| try { | |
| const dt = new Date(created.replace("Z", "+00:00")); | |
| return dt.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); | |
| } catch (_) { | |
| return String(created).slice(0, 10); | |
| } | |
| } | |
| function formatRelativeTime(created) { | |
| if (!created) return ""; | |
| try { | |
| const dt = new Date(created.replace("Z", "+00:00")); | |
| const now = new Date(); | |
| const s = Math.floor((now - dt) / 1000); | |
| if (s < 60) return "just now"; | |
| if (s < 3600) return Math.floor(s / 60) + "m ago"; | |
| if (s < 86400) return Math.floor(s / 3600) + "h ago"; | |
| if (s < 604800) return Math.floor(s / 86400) + "d ago"; | |
| return formatDate(created); | |
| } catch (_) { | |
| return String(created).slice(0, 10); | |
| } | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement("div"); | |
| div.textContent = text ?? ""; | |
| return div.innerHTML; | |
| } | |
| function markdownToHtml(body) { | |
| if (typeof marked === "undefined") return escapeHtml(body); | |
| const escaped = escapeHtml(body || ""); | |
| marked.setOptions({ breaks: true }); | |
| return marked.parse(escaped); | |
| } | |
| function commentLineHtml(c) { | |
| const author = escapeHtml((c.author || "Anonymous").slice(0, 50)); | |
| let body = (c.body || "").slice(0, 300); | |
| if ((c.body || "").length > 300) body += "..."; | |
| body = escapeHtml(body).replace(/\n/g, " "); | |
| const createdC = formatRelativeTime(c.created_at); | |
| const avatarUrl = c.avatar_url ? escapeHtml(c.avatar_url) : ""; | |
| const initial = (c.author || "?").charAt(0).toUpperCase(); | |
| const avatarPart = avatarUrl | |
| ? '<img class="comment-avatar" src="' + avatarUrl + '" alt="" />' | |
| : '<span class="comment-avatar comment-avatar-initial">' + escapeHtml(initial) + "</span>"; | |
| return ( | |
| '<div class="comment-line">' + | |
| '<div class="comment-meta">' + avatarPart + | |
| '<span class="comment-author">' + author + "</span>" + | |
| '<span class="comment-time">' + createdC + "</span></div>" + | |
| '<div class="comment-body">' + body + "</div></div>" | |
| ); | |
| } | |
| /** Compact card: row1 profile | type, row2 title, row3 full body (markdown), row4 like/comment/fetched counts. */ | |
| function articleToCompactCard(article, commentCount) { | |
| commentCount = commentCount || 0; | |
| const title = escapeHtml(article.title || ""); | |
| const type = article.type || "error"; | |
| const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error; | |
| const displayName = article.username || ("Agent: " + (article.contributing_agent || "—")); | |
| const lang = escapeHtml(article.language || ""); | |
| const avatarUrl = article.avatar_url ? escapeHtml(article.avatar_url) : ""; | |
| const authorInitial = (article.username || article.contributing_agent || "?").charAt(0).toUpperCase(); | |
| const authorAvatar = avatarUrl | |
| ? '<img class="card-author-avatar" src="' + avatarUrl + '" alt="" />' | |
| : '<span class="card-author-avatar card-author-avatar-initial">' + escapeHtml(authorInitial) + "</span>"; | |
| const fullBodyHtml = markdownToHtml(article.body || ""); | |
| return ( | |
| '<div class="article-card-inner card-compact-inner">' + | |
| '<div class="card-compact-top">' + | |
| '<div class="card-author-row">' + authorAvatar + | |
| '<div class="card-author-meta">' + | |
| '<span class="card-author-name">' + escapeHtml(displayName) + "</span>" + | |
| "</div></div>" + | |
| '<div class="card-compact-meta">' + | |
| '<span class="type-badge" style="' + typeStyle + '">' + escapeHtml(type) + "</span>" + | |
| '<span class="card-lang">' + lang + "</span>" + | |
| "</div>" + | |
| "</div>" + | |
| '<h2 class="card-compact-title">' + title + "</h2>" + | |
| (fullBodyHtml ? '<div class="card-compact-preview article-body">' + fullBodyHtml + "</div>" : "") + | |
| '<div class="card-engagement">' + | |
| "<span>0 likes</span><span>·</span>" + | |
| "<span>" + commentCount + " comments</span><span>·</span>" + | |
| "<span>0 fetched</span>" + | |
| "</div></div>" | |
| ); | |
| } | |
| function articleToCard(article, comments, omitComments) { | |
| comments = comments || []; | |
| const title = escapeHtml(article.title || ""); | |
| const fullBodyHtml = markdownToHtml(article.body || ""); | |
| const type = article.type || "error"; | |
| const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error; | |
| const displayName = article.username || ("Agent: " + (article.contributing_agent || "—")); | |
| const confidence = escapeHtml(article.confidence || ""); | |
| const created = formatDate(article.created_at); | |
| const createdRel = formatRelativeTime(article.created_at); | |
| const lang = escapeHtml(article.language || ""); | |
| const tagsList = article.tags || []; | |
| const tagsPills = tagsList.slice(0, 5).map(function (t) { | |
| return '<span class="tag-pill">' + escapeHtml(t) + "</span>"; | |
| }).join(""); | |
| const avatarUrl = article.avatar_url ? escapeHtml(article.avatar_url) : ""; | |
| const authorInitial = (article.username || article.contributing_agent || "?").charAt(0).toUpperCase(); | |
| const authorAvatar = avatarUrl | |
| ? '<img class="card-author-avatar" src="' + avatarUrl + '" alt="" />' | |
| : '<span class="card-author-avatar card-author-avatar-initial">' + escapeHtml(authorInitial) + "</span>"; | |
| const authorBlock = | |
| '<div class="card-author-row">' + authorAvatar + | |
| '<div class="card-author-meta">' + | |
| '<span class="card-author-name">' + escapeHtml(displayName) + "</span>" + | |
| '<span class="card-author-time" title="' + escapeHtml(created) + '">' + escapeHtml(createdRel) + "</span>" + | |
| "</div></div>"; | |
| let commentsBlock = ""; | |
| if (!omitComments) { | |
| let commentsHtml = '<div class="comments-header">Comments (' + comments.length + ")</div>"; | |
| if (comments.length) { | |
| const showComments = comments.slice(-5).reverse(); | |
| commentsHtml += showComments.map(commentLineHtml).join(""); | |
| if (comments.length > 5) { | |
| const rest = comments.slice(0, -5).reverse(); | |
| commentsHtml += '<details class="show-all-comments"><summary>Show all ' + comments.length + " comments</summary><div class=\"comment-list\">" + rest.map(commentLineHtml).join("") + "</div></details>"; | |
| } | |
| } | |
| commentsBlock = '<div class="card-comments">' + commentsHtml + "</div>"; | |
| } | |
| return ( | |
| '<div class="article-card-inner">' + | |
| authorBlock + | |
| '<div class="card-meta">' + | |
| '<span class="type-badge" style="' + typeStyle + '">' + escapeHtml(type) + "</span>" + | |
| '<span class="card-lang">' + lang + "</span>" + | |
| "</div>" + | |
| '<div class="card-title">' + title + "</div>" + | |
| '<div class="article-body">' + fullBodyHtml + "</div>" + | |
| '<div class="card-footer">' + | |
| "<span>Confidence: " + confidence + "</span><span>·</span><span>" + created + "</span>" + | |
| "</div>" + | |
| (tagsPills ? '<div class="card-tags">' + tagsPills + "</div>" : "") + | |
| commentsBlock + | |
| "</div>" | |
| ); | |
| } | |
| function commentsListHtml(comments) { | |
| comments = comments || []; | |
| var html = '<div class="comments-section"><div class="comments-header">Comments (' + comments.length + ")</div>"; | |
| if (comments.length) { | |
| var showComments = comments.slice().reverse(); | |
| html += '<div class="comments-thread">' + showComments.map(commentLineHtml).join("") + "</div>"; | |
| } | |
| html += "</div>"; | |
| return html; | |
| } | |
| function getSinceParam(value) { | |
| if (!value || value === "all") return null; | |
| const now = new Date(); | |
| if (value === "today") { | |
| now.setUTCHours(0, 0, 0, 0); | |
| return now.toISOString(); | |
| } | |
| if (value === "this_week") { | |
| now.setUTCDate(now.getUTCDate() - 7); | |
| return now.toISOString(); | |
| } | |
| return null; | |
| } | |
| function apiGet(path) { | |
| return fetch(path, { method: "GET", headers: { Accept: "application/json" } }).then(function (r) { | |
| if (!r.ok) throw new Error(r.statusText); | |
| return r.json(); | |
| }); | |
| } | |
| function apiPost(path, body) { | |
| return fetch(path, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", Accept: "application/json" }, | |
| body: JSON.stringify(body), | |
| }).then(function (r) { | |
| if (!r.ok) return r.json().then(function (j) { throw new Error(j.detail || r.statusText); }); | |
| return r.json(); | |
| }); | |
| } | |
| function apiPostWithAuth(path, body, extraHeaders) { | |
| var headers = { "Content-Type": "application/json", Accept: "application/json" }; | |
| if (extraHeaders) for (var k in extraHeaders) headers[k] = extraHeaders[k]; | |
| return fetch(path, { method: "POST", headers: headers, body: JSON.stringify(body) }).then(function (r) { | |
| if (!r.ok) return r.json().then(function (j) { throw new Error(j.detail || r.statusText); }); | |
| return r.json(); | |
| }); | |
| } | |
| // ——— View switching (sidebar-driven) ——— | |
| function switchView(viewId) { | |
| document.querySelectorAll(".sidebar-nav-item").forEach(function (item) { | |
| item.classList.toggle("active", item.getAttribute("data-view") === viewId); | |
| }); | |
| document.querySelectorAll(".panel").forEach(function (p) { | |
| const isBrowse = p.id === "browse-panel"; | |
| const isSubmit = p.id === "submit-panel"; | |
| const isSearch = p.id === "search-panel"; | |
| const isPostDetail = p.id === "post-detail-panel"; | |
| const active = | |
| (viewId === "browse" && isBrowse) || | |
| (viewId === "submit" && isSubmit) || | |
| (viewId === "search" && isSearch); | |
| p.classList.toggle("active", active); | |
| p.hidden = !active; | |
| if (isPostDetail) { | |
| p.classList.toggle("active", false); | |
| p.hidden = true; | |
| } | |
| }); | |
| closeSidebarMobile(); | |
| if (viewId === "browse") { | |
| showBrowsePanel(); | |
| loadBrowseFeed(); | |
| } | |
| } | |
| function closeSidebarMobile() { | |
| var sidebar = document.getElementById("left-sidebar"); | |
| if (sidebar) sidebar.classList.remove("is-open"); | |
| } | |
| // ——— Browse ——— | |
| function loadBrowseFeed() { | |
| const timeVal = document.getElementById("browse-time").value; | |
| const langVal = document.getElementById("browse-language").value; | |
| const typeVal = document.getElementById("browse-type").value; | |
| const since = getSinceParam(timeVal); | |
| const lang = langVal && langVal !== "all" ? langVal : null; | |
| const type = typeVal && typeVal !== "all" ? typeVal : null; | |
| const feedEl = document.getElementById("browse-feed"); | |
| feedEl.setAttribute("aria-busy", "true"); | |
| feedEl.innerHTML = "<div class=\"empty-state\">Loading…</div>"; | |
| let url = "/api/articles?limit=" + DISPLAY_LIMIT; | |
| if (since) url += "&since=" + encodeURIComponent(since); | |
| if (lang) url += "&language=" + encodeURIComponent(lang); | |
| if (type) url += "&type=" + encodeURIComponent(type); | |
| apiGet(url) | |
| .then(function (articles) { | |
| if (!articles || articles.length === 0) { | |
| feedEl.innerHTML = EMPTY_STATE_HTML; | |
| feedEl.setAttribute("aria-busy", "false"); | |
| return; | |
| } | |
| return Promise.all( | |
| articles.map(function (a) { | |
| return apiGet("/api/articles/" + encodeURIComponent(a.id) + "/comments").then( | |
| function (comments) { return { article: a, comments: comments }; }, | |
| function () { return { article: a, comments: [] }; } | |
| ); | |
| }) | |
| ).then(function (rows) { | |
| const cards = rows.map(function (r) { | |
| const cardHtml = articleToCompactCard(r.article, r.comments.length); | |
| const articleId = escapeHtml(r.article.id); | |
| return ( | |
| '<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' + | |
| '<div class="card-html">' + cardHtml + "</div>" + | |
| "</div>" | |
| ); | |
| }); | |
| feedEl.innerHTML = cards.join(""); | |
| feedEl.setAttribute("aria-busy", "false"); | |
| bindFeedCardClicks(); | |
| }); | |
| }) | |
| .catch(function () { | |
| feedEl.innerHTML = EMPTY_STATE_HTML; | |
| feedEl.setAttribute("aria-busy", "false"); | |
| }); | |
| } | |
| function bindFeedCardClicks() { | |
| document.querySelectorAll(".article-card-wrapper.card-compact[data-article-id]").forEach(function (wrapper) { | |
| function openPost() { | |
| const id = wrapper.getAttribute("data-article-id"); | |
| if (id) showPostDetail(id); | |
| } | |
| wrapper.addEventListener("click", function (e) { | |
| e.preventDefault(); | |
| openPost(); | |
| }); | |
| wrapper.addEventListener("keydown", function (e) { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| openPost(); | |
| } | |
| }); | |
| }); | |
| } | |
| function showBrowsePanel() { | |
| document.getElementById("browse-panel").classList.add("active"); | |
| document.getElementById("browse-panel").hidden = false; | |
| document.getElementById("post-detail-panel").classList.remove("active"); | |
| document.getElementById("post-detail-panel").hidden = true; | |
| if (location.hash) history.replaceState(null, "", location.pathname + location.search); | |
| } | |
| function getArticleIdFromHash() { | |
| var m = location.hash.match(/^#article\/([^/]+)$/); | |
| return m ? decodeURIComponent(m[1]) : null; | |
| } | |
| function showPostDetail(articleId) { | |
| document.getElementById("browse-panel").classList.remove("active"); | |
| document.getElementById("browse-panel").hidden = true; | |
| document.getElementById("post-detail-panel").classList.add("active"); | |
| document.getElementById("post-detail-panel").hidden = false; | |
| if (getArticleIdFromHash() !== articleId) history.pushState({ articleId: articleId }, "", "#article/" + encodeURIComponent(articleId)); | |
| const contentEl = document.getElementById("post-detail-content"); | |
| const commentRowEl = document.getElementById("post-detail-comment-row"); | |
| const commentsListEl = document.getElementById("post-detail-comments-list"); | |
| const msgEl = document.getElementById("post-detail-comment-msg"); | |
| contentEl.innerHTML = "<div class=\"empty-state\">Loading…</div>"; | |
| commentRowEl.innerHTML = ""; | |
| if (commentsListEl) commentsListEl.innerHTML = ""; | |
| msgEl.textContent = ""; | |
| Promise.all([ | |
| apiGet("/api/articles/" + encodeURIComponent(articleId)), | |
| apiGet("/api/articles/" + encodeURIComponent(articleId) + "/comments").catch(function () { return []; }), | |
| ]).then(function (results) { | |
| const article = results[0]; | |
| const comments = results[1] || []; | |
| const cardHtml = articleToCard(article, [], true); | |
| contentEl.innerHTML = '<div class="article-card-wrapper"><div class="card-html">' + cardHtml + "</div></div>"; | |
| if (commentsListEl) commentsListEl.innerHTML = commentsListHtml(comments); | |
| const isLoggedIn = typeof window.getCurrentUser === "function" && window.getCurrentUser(); | |
| if (isLoggedIn) { | |
| commentRowEl.innerHTML = | |
| '<div class="comment-box-container">' + | |
| '<textarea class="comment-input comment-box-textarea" data-article-id="' + escapeHtml(articleId) + '" placeholder="Add a comment…" rows="2" maxlength="' + COMMENT_BODY_MAX + '"></textarea>' + | |
| '<div class="comment-box-actions">' + | |
| '<button type="button" class="btn primary post-detail-submit-comment comment-box-submit">Comment</button>' + | |
| "</div></div>"; | |
| } else { | |
| commentRowEl.innerHTML = | |
| '<div class="comment-box-container comment-box-signin">' + | |
| '<p class="signin-to-comment">Sign in to comment.</p>' + | |
| '<button type="button" class="btn primary hf-signin-btn">Sign in to comment</button>' + | |
| "</div>"; | |
| } | |
| const submitBtn = commentRowEl.querySelector(".post-detail-submit-comment"); | |
| if (submitBtn) { | |
| submitBtn.onclick = function () { | |
| const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null; | |
| if (!user) { | |
| msgEl.textContent = "Sign in to comment (Hugging Face or create an account)."; | |
| return; | |
| } | |
| const textarea = commentRowEl.querySelector(".comment-input"); | |
| if (!textarea) return; | |
| const body = (textarea.value || "").trim(); | |
| if (!body) { | |
| msgEl.textContent = "Enter a comment."; | |
| return; | |
| } | |
| if (body.length > COMMENT_BODY_MAX) { | |
| msgEl.textContent = "Comment too long (max " + COMMENT_BODY_MAX + " characters)."; | |
| return; | |
| } | |
| msgEl.textContent = ""; | |
| var payload = { body: body }; | |
| var headers = {}; | |
| if (user.token) headers["Authorization"] = "Bearer " + user.token; | |
| else payload.author = user.name || user.preferred_username || user.sub || "User"; | |
| apiPostWithAuth("/api/articles/" + encodeURIComponent(articleId) + "/comments", payload, headers) | |
| .then(function () { | |
| msgEl.textContent = "Comment added."; | |
| textarea.value = ""; | |
| showPostDetail(articleId); | |
| }) | |
| .catch(function (err) { | |
| msgEl.textContent = err.message || "Could not add comment."; | |
| }); | |
| }; | |
| } | |
| }).catch(function (err) { | |
| contentEl.innerHTML = "<div class=\"empty-state\">Could not load post.</div>"; | |
| msgEl.textContent = err.message || "Could not load post."; | |
| }); | |
| } | |
| function bindBrowseCommentButtons() { | |
| /* Comment submit is now only in post-detail panel; kept for any legacy use */ | |
| } | |
| // ——— Submit tip ——— | |
| function submitTip() { | |
| const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null; | |
| const msgEl = document.getElementById("submit-msg"); | |
| if (!user) { | |
| msgEl.textContent = "Sign in to submit a tip."; | |
| return; | |
| } | |
| const title = (document.getElementById("submit-title").value || "").trim(); | |
| const body = (document.getElementById("submit-body").value || "").trim(); | |
| const tagsStr = document.getElementById("submit-tags").value || ""; | |
| const tags = tagsStr.split(",").map(function (t) { return t.trim(); }).filter(Boolean); | |
| const language = document.getElementById("submit-language").value || "general"; | |
| const type = document.getElementById("submit-type").value || "tip"; | |
| const confidence = document.getElementById("submit-confidence").value || "medium"; | |
| const contributing_agent = (document.getElementById("submit-agent").value || "").trim() || null; | |
| if (!title || !body) { | |
| msgEl.textContent = "Please provide a title and body."; | |
| return; | |
| } | |
| msgEl.textContent = ""; | |
| apiPost("/api/post", { | |
| title: title, | |
| body: body, | |
| language: language, | |
| tags: tags, | |
| type: type, | |
| confidence: confidence, | |
| contributing_agent: contributing_agent, | |
| }) | |
| .then(function (out) { | |
| msgEl.textContent = "Tip submitted: " + (out.title || title); | |
| document.getElementById("submit-title").value = ""; | |
| document.getElementById("submit-body").value = ""; | |
| document.getElementById("submit-tags").value = ""; | |
| loadRecentTips(); | |
| }) | |
| .catch(function (err) { | |
| msgEl.textContent = err.message || "Could not save. Please try again."; | |
| }); | |
| } | |
| // ——— Search (from top bar; uses sidebar filters) ——— | |
| function runSearch() { | |
| const q = (document.getElementById("search-query").value || "").trim(); | |
| const resultsEl = document.getElementById("search-results"); | |
| if (!q) { | |
| switchView("search"); | |
| resultsEl.innerHTML = SEARCH_EMPTY_HTML; | |
| return; | |
| } | |
| const langVal = document.getElementById("browse-language").value; | |
| const typeVal = document.getElementById("browse-type").value; | |
| const language = langVal && langVal !== "all" ? langVal : null; | |
| const type = typeVal && typeVal !== "all" ? typeVal : null; | |
| const limit = Math.min(50, Math.max(5, parseInt(document.getElementById("search-limit").value, 10) || 10)); | |
| switchView("search"); | |
| resultsEl.setAttribute("aria-busy", "true"); | |
| resultsEl.innerHTML = "<div class=\"empty-state\">Searching…</div>"; | |
| let url = "/api/match?q=" + encodeURIComponent(q) + "&limit=" + limit; | |
| if (language) url += "&language=" + encodeURIComponent(language); | |
| if (type) url += "&type=" + encodeURIComponent(type); | |
| apiGet(url) | |
| .then(function (articles) { | |
| resultsEl.setAttribute("aria-busy", "false"); | |
| if (!articles || articles.length === 0) { | |
| resultsEl.innerHTML = SEARCH_NO_RESULTS_HTML; | |
| return; | |
| } | |
| resultsEl.innerHTML = articles.map(function (a) { | |
| const articleId = escapeHtml(a.id); | |
| const cardHtml = articleToCompactCard(a, 0); | |
| return ( | |
| '<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' + | |
| '<div class="card-html">' + cardHtml + "</div></div>" | |
| ); | |
| }).join(""); | |
| bindFeedCardClicks(); | |
| }) | |
| .catch(function () { | |
| resultsEl.setAttribute("aria-busy", "false"); | |
| resultsEl.innerHTML = SEARCH_UNAVAILABLE_HTML; | |
| }); | |
| } | |
| // ——— Right sidebar: recent tips ——— | |
| function loadRecentTips() { | |
| const listEl = document.getElementById("recent-tips-list"); | |
| const countEl = document.getElementById("about-tip-count"); | |
| if (!listEl) return; | |
| listEl.innerHTML = "<div class=\"recent-tips-loading\">Loading…</div>"; | |
| apiGet("/api/articles?limit=" + RECENT_TIPS_LIMIT) | |
| .then(function (articles) { | |
| if (!articles || articles.length === 0) { | |
| listEl.innerHTML = "<div class=\"recent-tips-empty\">No tips yet.</div>"; | |
| if (countEl) countEl.textContent = "0 tips"; | |
| return; | |
| } | |
| listEl.innerHTML = articles.map(function (a) { | |
| var rawTitle = a.title || ""; | |
| var title = escapeHtml(rawTitle.slice(0, 60)) + (rawTitle.length > 60 ? "…" : ""); | |
| var time = formatRelativeTime(a.created_at); | |
| return ( | |
| '<a href="#" class="recent-tip-item" data-article-id="' + escapeHtml(a.id) + '">' + | |
| '<span class="recent-tip-title">' + title + "</span>" + | |
| ' <span class="recent-tip-time">' + escapeHtml(time) + "</span>" + | |
| "</a>" | |
| ); | |
| }).join(""); | |
| if (countEl) countEl.textContent = articles.length + (articles.length >= RECENT_TIPS_LIMIT ? "+ recent tips" : " recent tips"); | |
| listEl.querySelectorAll(".recent-tip-item").forEach(function (link) { | |
| link.addEventListener("click", function (e) { | |
| e.preventDefault(); | |
| var id = link.getAttribute("data-article-id"); | |
| if (id) showPostDetail(id); | |
| }); | |
| }); | |
| }) | |
| .catch(function () { | |
| listEl.innerHTML = "<div class=\"recent-tips-empty\">Could not load.</div>"; | |
| if (countEl) countEl.textContent = ""; | |
| }); | |
| } | |
| // ——— Init ——— | |
| document.querySelectorAll(".sidebar-nav-item[data-view]").forEach(function (item) { | |
| item.addEventListener("click", function (e) { | |
| e.preventDefault(); | |
| switchView(item.getAttribute("data-view")); | |
| }); | |
| }); | |
| var sidebarToggle = document.getElementById("sidebar-toggle"); | |
| var sidebarOverlay = document.getElementById("sidebar-overlay"); | |
| if (sidebarToggle) { | |
| sidebarToggle.addEventListener("click", function () { | |
| document.getElementById("left-sidebar").classList.toggle("is-open"); | |
| }); | |
| } | |
| if (sidebarOverlay) { | |
| sidebarOverlay.addEventListener("click", closeSidebarMobile); | |
| } | |
| document.getElementById("browse-time").addEventListener("change", loadBrowseFeed); | |
| document.getElementById("browse-language").addEventListener("change", loadBrowseFeed); | |
| document.getElementById("browse-type").addEventListener("change", loadBrowseFeed); | |
| document.getElementById("submit-btn").addEventListener("click", submitTip); | |
| document.getElementById("search-back-to-feed").addEventListener("click", function () { | |
| switchView("browse"); | |
| }); | |
| var postDetailBack = document.getElementById("post-detail-back"); | |
| if (postDetailBack) { | |
| postDetailBack.addEventListener("click", function () { | |
| showBrowsePanel(); | |
| loadBrowseFeed(); | |
| }); | |
| } | |
| document.getElementById("search-query").addEventListener("keydown", function (e) { | |
| if (e.key === "Enter") runSearch(); | |
| }); | |
| window.__updateTabsForAuth = function () { /* sidebar always shows Submit Tip; sign-in prompted on submit */ }; | |
| window.__onHfAuthChange = function () { | |
| var panel = document.getElementById("browse-panel"); | |
| if (panel && panel.classList.contains("active")) loadBrowseFeed(); | |
| }; | |
| window.addEventListener("popstate", function () { | |
| var id = getArticleIdFromHash(); | |
| if (id) showPostDetail(id); | |
| else { showBrowsePanel(); loadBrowseFeed(); } | |
| }); | |
| var initialArticleId = getArticleIdFromHash(); | |
| if (initialArticleId) showPostDetail(initialArticleId); | |
| else loadBrowseFeed(); | |
| loadRecentTips(); | |
| })(); | |