PageSourceSearch

https://orzbar.com/comments.js?v=3

js orzbar.com collected 2026-10-02 01:38:39 UTC 8,021 bytes, 212 lines download raw bytes

1(() => {
2  const LANG_KEY = "orzbar_lang";
3  const root = document.querySelector("#commentsSection");
4  if (!root) return;
5
6  const slug = new URLSearchParams(window.location.search).get("slug");
7  if (!slug) {
8    root.hidden = true;
9    return;
10  }
11
12  const copy = {
13    zh: {
14      title: "留言",
15      intro: "可以匿名,也可以留下名字。留言会公开显示在这篇文章下。",
16      name: "名字(可选)",
17      namePlaceholder: "匿名",
18      message: "留言",
19      messagePlaceholder: "写下你的想法…",
20      submit: "发布留言",
21      sending: "正在发布…",
22      empty: "还没有留言。来写第一条吧。",
23      loadMore: "加载更早留言",
24      anonymous: "匿名访客",
25      success: "留言已发布。",
26      failure: "暂时无法发布,请稍后再试。",
27      captcha: "简单验证",
28    },
29    en: {
30      title: "Comments",
31      intro: "Post anonymously or leave a name. Your comment will appear publicly below this article.",
32      name: "Name (optional)",
33      namePlaceholder: "Anonymous",
34      message: "Comment",
35      messagePlaceholder: "Share a thought…",
36      submit: "Post comment",
37      sending: "Posting…",
38      empty: "No comments yet. Start the conversation.",
39      loadMore: "Load earlier comments",
40      anonymous: "Anonymous visitor",
41      success: "Your comment is live.",
42      failure: "Could not post right now. Please try again.",
43      captcha: "Quick check",
44    },
45    fr: {
46      title: "Commentaires",
47      intro: "Publiez anonymement ou indiquez votre nom. Votre commentaire apparaîtra publiquement sous cet article.",
48      name: "Nom (facultatif)",
49      namePlaceholder: "Anonyme",
50      message: "Commentaire",
51      messagePlaceholder: "Partagez votre point de vue…",
52      submit: "Publier",
53      sending: "Publication…",
54      empty: "Aucun commentaire pour le moment. Lancez la discussion.",
55      loadMore: "Afficher les commentaires précédents",
56      anonymous: "Visiteur anonyme",
57      success: "Votre commentaire est publié.",
58      failure: "Publication impossible pour le moment. Veuillez réessayer.",
59      captcha: "Vérification rapide",
60    },
61  };
62
63  let comments = [];
64  let nextBeforeId = null;
65  let captchaEnabled = false;
66  let captchaToken = "";
67
68  function language() {
69    const lang = new URLSearchParams(window.location.search).get("lang") || localStorage.getItem(LANG_KEY);
70    return ["zh", "en", "fr"].includes(lang) ? lang : "en";
71  }
72
73  function text() {
74    return copy[language()];
75  }
76
77  function setStatus(message, kind = "") {
78    const status = root.querySelector("#commentStatus");
79    if (!status) return;
80    status.textContent = message;
81    status.dataset.kind = kind;
82  }
83
84  function commentElement(comment) {
85    const article = document.createElement("article");
86    article.className = "comment-item";
87    article.id = `comment-${comment.id}`;
88    const header = document.createElement("header");
89    const author = document.createElement("strong");
90    author.textContent = comment.author || text().anonymous;
91    const time = document.createElement("time");
92    time.dateTime = comment.createdAt;
93    const locale = language() === "zh" ? "zh-CN" : language() === "fr" ? "fr-FR" : "en-US";
94    time.textContent = new Date(comment.createdAt).toLocaleString(locale, {
95      year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
96    });
97    const body = document.createElement("p");
98    body.textContent = comment.body;
99    header.append(author, time);
100    article.append(header, body);
101    return article;
102  }
103
104  // 从统计页或提醒邮件点进来的 #comment-<id>:留言列表是异步加载的,渲染后再定位一次。
105  let linkedCommentFocused = false;
106  function focusLinkedComment() {
107    if (linkedCommentFocused) return;
108    const match = /^#comment-(\d+)$/.exec(window.location.hash || "");
109    if (!match) return;
110    const target = document.getElementById(`comment-${match[1]}`);
111    if (!target) return;
112    linkedCommentFocused = true;
113    target.classList.add("comment-linked");
114    target.scrollIntoView({ behavior: "smooth", block: "center" });
115  }
116
117  function renderList() {
118    const list = root.querySelector("#commentList");
119    const empty = root.querySelector("#commentEmpty");
120    const more = root.querySelector("#commentMore");
121    if (!list || !empty || !more) return;
122    list.replaceChildren(...comments.map(commentElement));
123    focusLinkedComment();
124    empty.textContent = text().empty;
125    empty.hidden = comments.length !== 0;
126    more.textContent = text().loadMore;
127    more.hidden = !nextBeforeId;
128  }
129
130  async function loadCaptcha() {
131    const field = root.querySelector("#commentCaptchaField");
132    if (!captchaEnabled || !field) {
133      if (field) field.hidden = true;
134      return;
135    }
136    const response = await fetch("/api/comments/captcha", { cache: "no-store" });
137    if (!response.ok) throw new Error("captcha unavailable");
138    const challenge = await response.json();
139    captchaToken = challenge.token;
140    field.hidden = false;
141    field.querySelector("span").textContent = `${text().captcha}: ${challenge.question}`;
142    field.querySelector("input").value = "";
143  }
144
145  function renderShell() {
146    const t = text();
147    root.querySelector("h2").textContent = t.title;
148    root.querySelector(".comments-intro").textContent = t.intro;
149    root.querySelector("label[for=commentAuthor] span").textContent = t.name;
150    root.querySelector("#commentAuthor").placeholder = t.namePlaceholder;
151    root.querySelector("label[for=commentBody] span").textContent = t.message;
152    root.querySelector("#commentBody").placeholder = t.messagePlaceholder;
153    root.querySelector("#commentSubmit").textContent = t.submit;
154    renderList();
155    if (captchaEnabled) loadCaptcha().catch(() => setStatus(t.failure, "error"));
156  }
157
158  async function loadComments(append = false) {
159    const suffix = append && nextBeforeId ? `&before=${encodeURIComponent(nextBeforeId)}` : "";
160    const response = await fetch(`/api/comments?slug=${encodeURIComponent(slug)}${suffix}`, { cache: "no-store" });
161    if (!response.ok) throw new Error("comments unavailable");
162    const payload = await response.json();
163    comments = append ? comments.concat(payload.comments) : payload.comments;
164    nextBeforeId = payload.nextBeforeId;
165    captchaEnabled = payload.captchaEnabled === true;
166    renderList();
167    if (captchaEnabled && !append) await loadCaptcha();
168  }
169
170  root.querySelector("form").addEventListener("submit", async (event) => {
171    event.preventDefault();
172    const t = text();
173    const button = root.querySelector("#commentSubmit");
174    button.disabled = true;
175    button.textContent = t.sending;
176    setStatus("");
177    try {
178      const response = await fetch("/api/comments", {
179        method: "POST",
180        headers: { "content-type": "application/json" },
181        body: JSON.stringify({
182          postSlug: slug,
183          author: root.querySelector("#commentAuthor").value,
184          body: root.querySelector("#commentBody").value,
185          captchaToken,
186          captchaAnswer: root.querySelector("#commentCaptchaAnswer").value,
187        }),
188      });
189      if (!response.ok) throw new Error("post failed");
190      const payload = await response.json();
191      comments.unshift(payload.comment);
192      root.querySelector("#commentBody").value = "";
193      setStatus(t.success, "success");
194      renderList();
195      if (captchaEnabled) await loadCaptcha();
196    } catch {
197      setStatus(t.failure, "error");
198      if (captchaEnabled) loadCaptcha().catch(() => {});
199    } finally {
200      button.disabled = false;
201      button.textContent = t.submit;
202    }
203  });
204
205  root.querySelector("#commentMore").addEventListener("click", () => {
206    loadComments(true).catch(() => setStatus(text().failure, "error"));
207  });
208  window.addEventListener("orzbar:languagechange", renderShell);
209
210  renderShell();
211  loadComments().catch(() => setStatus(text().failure, "error"));
212})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.