5 Angajamente 61b9a027fe ... ef964669b6

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  xander ef964669b6 Widget: remove telegram icon from header 1 săptămână în urmă
  xander e16932bfd1 Widget: full channel feed (75 posts) with pagination via fetch script and 'Показать ещё' chunks 1 săptămână în urmă
  xander 6f3c5e06c7 Widget footer: blue 'Перейти в телеграм' bar instead of fake input 1 săptămână în urmă
  xander 6480b84a1d Widget: non-clickable photos, fix invisible post text (blockquote inside p broke HTML), Telegram-style quote 1 săptămână în urmă
  xander fc2260dfb4 Telegram widget: fetch all page posts, video support, remove wallpaper pattern and external-link icons, bigger logo 1 săptămână în urmă
3 a modificat fișierele cu 100 adăugiri și 46 ștergeri
  1. 55 27
      scripts/fetch-telegram.mjs
  2. 44 19
      src/components/TelegramWidget.astro
  3. 1 0
      src/data/telegram-news.json

+ 55 - 27
scripts/fetch-telegram.mjs

@@ -1,43 +1,71 @@
-// Fetches the public Telegram channel feed (t.me/s/...) and saves the latest
-// posts for the home-page Telegram widget. Run manually: npm run fetch-news
+// Fetches the public Telegram channel feed (t.me/s/...) — the whole first page
+// chain via ?before=<id> pagination — and saves the posts for the home-page
+// Telegram widget. Run manually: npm run fetch-news
 import * as cheerio from "cheerio";
 import { mkdir, writeFile } from "node:fs/promises";
 import path from "node:path";
 
 const CHANNEL = "scientia_ru";
+const MAX_PAGES = 40; // safety cap (~20 posts per page)
 const UA =
   "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
 
-const res = await fetch(`https://t.me/s/${CHANNEL}`, { headers: { "User-Agent": UA } });
-if (!res.ok) {
-  console.error(`Failed to fetch channel: HTTP ${res.status}`);
-  process.exit(1);
-}
-const $ = cheerio.load(await res.text());
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
 
 const posts = [];
-$(".tgme_widget_message").each((_, el) => {
-  const $el = $(el);
-  const textNode = $el.find(".tgme_widget_message_text").first();
-  // Keep basic formatting tags, drop telegram-internal spans/attrs
-  textNode.find("a, b, i, u, s, strong, em, code, br").each((__, keep) => {
-    const attrs = $(keep).attr() || {};
-    for (const name of Object.keys(attrs)) {
-      if (name !== "href") $(keep).removeAttr(name);
+const seen = new Set();
+let before = null;
+
+for (let page = 0; page < MAX_PAGES; page++) {
+  const url = `https://t.me/s/${CHANNEL}${before ? `?before=${before}` : ""}`;
+  const res = await fetch(url, { headers: { "User-Agent": UA } });
+  if (!res.ok) {
+    console.error(`page ${page}: HTTP ${res.status}`);
+    break;
+  }
+  const $ = cheerio.load(await res.text());
+  let newCount = 0;
+  let oldestId = null;
+
+  $(".tgme_widget_message").each((_, el) => {
+    const $el = $(el);
+    const postId = ($el.attr("data-post") || "").split("/")[1];
+    if (!postId || seen.has(postId)) return;
+    seen.add(postId);
+    newCount++;
+    oldestId = postId;
+
+    const textNode = $el.find(".tgme_widget_message_text").first();
+    // Keep basic formatting tags, drop telegram-internal attributes
+    textNode.find("a, b, i, u, s, strong, em, code, br, blockquote").each((__, keep) => {
+      const attrs = $(keep).attr() || {};
+      for (const name of Object.keys(attrs)) {
+        if (name !== "href") $(keep).removeAttr(name);
+      }
+    });
+    const text = textNode.html()?.trim() || "";
+    const datetime = $el.find("time").first().attr("datetime") || "";
+    const photoStyle = $el.find(".tgme_widget_message_photo_wrap").first().attr("style") || "";
+    const photo = photoStyle.match(/url\('([^']+)'\)/)?.[1]?.split("?")[0] || "";
+    const video = $el.find(".tgme_widget_message_video_wrap, .tgme_widget_message_video_player").length > 0;
+    const link = $el.find(".tgme_widget_message_date").first().attr("href") || `https://t.me/${CHANNEL}/${postId}`;
+    if (text || photo || video) {
+      posts.push({ id: Number(postId), text, datetime, photo, video, link });
     }
   });
-  const text = textNode.html()?.trim() || "";
-  const datetime = $el.find("time").first().attr("datetime") || "";
-  const photoStyle = $el.find(".tgme_widget_message_photo_wrap").first().attr("style") || "";
-  const photo = photoStyle.match(/url\('([^']+)'\)/)?.[1]?.split("?")[0] || "";
-  const link = $el.find(".tgme_widget_message_date").first().attr("href") || `https://t.me/${CHANNEL}`;
-  if (text || photo) posts.push({ text, datetime, photo, link });
-});
-
-const latest = posts.slice(-4).reverse();
+
+  console.log(`page ${page}: +${newCount} posts (total kept ${posts.length})`);
+  if (newCount === 0 || !oldestId) break;
+  before = oldestId;
+  await sleep(300);
+}
+
+// newest first
+posts.sort((a, b) => b.id - a.id);
+
 await mkdir(path.dirname("src/data/telegram-news.json"), { recursive: true });
-await writeFile("src/data/telegram-news.json", JSON.stringify(latest, null, 2));
-console.log(`saved ${latest.length} posts`);
+await writeFile("src/data/telegram-news.json", JSON.stringify(posts, null, 2));
+console.log(`saved ${posts.length} posts`);
 
 // Channel avatar for the widget header
 const avatar = await fetch(`https://t.me/i/userpic/320/${CHANNEL}.jpg`, { headers: { "User-Agent": UA } });

+ 44 - 19
src/components/TelegramWidget.astro

@@ -18,54 +18,62 @@ const fmt = new Intl.DateTimeFormat("ru-RU", { day: "numeric", month: "long", ho
           канал · @scientia_ru
         </p>
       </div>
-      <svg class="h-6 w-6 shrink-0 text-[#2AABEE]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
-        <path d="M21.9 4.6c.2-1.2-.9-2.1-2-1.7L2.7 9.6c-1.2.5-1.1 2.2.1 2.6l4.4 1.4 1.7 5.4c.3 1.1 1.7 1.4 2.4.5l2.4-2.9 4.5 3.3c1 .7 2.4.2 2.6-1L21.9 4.6z" />
-      </svg>
     </div>
 
     <!-- Messages: fixed-height scroll area. Bubbles are divs because post text may contain links (nested <a> is invalid HTML) -->
     <div class="tg-wallpaper relative">
       <div class="tg-body space-y-2.5 p-3 pb-5">
-        {telegramNews.map((post) => (
-          <div class="tg-bubble relative ml-3 block rounded-xl bg-white px-3 py-2.5 shadow-[0_1px_1px_rgba(16,35,47,0.14)]">
+        {telegramNews.map((post, i) => (
+          <div class:list={["tg-bubble relative ml-3 block rounded-xl bg-white px-3 py-2.5 shadow-[0_1px_1px_rgba(16,35,47,0.14)]", i >= 8 && "hidden"]}>
             {post.photo && (
-              <a href={post.link} target="_blank" rel="noopener" class="block">
-                <img src={post.photo} alt="" class="mb-2 w-full rounded-lg" loading="lazy" />
-              </a>
+              <img src={post.photo} alt="" class="mb-2 w-full rounded-lg" loading="lazy" />
+            )}
+            {post.video && (
+              <span class="mb-2 flex aspect-video w-full items-center justify-center gap-2 rounded-lg bg-night-950/90 text-sm font-medium text-white">
+                <svg class="h-8 w-8" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
+                  <path d="M8 5.14v14l11-7-11-7z" />
+                </svg>
+                Видео
+              </span>
             )}
             {post.text && (
-              <p class="text-[13px] leading-snug text-night-950 [&_a]:break-words [&_a]:text-[#2AABEE]">
+              <div class="text-[13px] leading-snug text-night-950 [&_a]:break-words [&_a]:text-[#2AABEE]">
                 <Fragment set:html={post.text} />
-              </p>
+              </div>
             )}
-            {!post.text && <p class="text-[13px] text-slate-500">Фото</p>}
+            {!post.text && !(post.photo || post.video) && <p class="text-[13px] text-slate-500">Фото</p>}
             <a
               href={post.link}
               target="_blank"
               rel="noopener"
               class="mt-1 block text-right text-[11px] text-slate-400 transition-colors hover:text-[#2AABEE]"
             >
-              {post.datetime ? fmt.format(new Date(post.datetime)) : "Открыть пост"} ↗
+              {post.datetime ? fmt.format(new Date(post.datetime)) : "Открыть пост"}
             </a>
           </div>
         ))}
+        <button
+          type="button"
+          data-tg-more
+          class="mx-auto block rounded-full bg-white px-5 py-2 text-xs font-semibold text-[#2AABEE] shadow-sm transition-colors hover:bg-[#2AABEE] hover:text-white"
+        >
+          Показать ещё
+        </button>
       </div>
       <!-- soft fade over the scroll area -->
       <div class="pointer-events-none absolute inset-x-0 bottom-0 h-7 bg-gradient-to-t from-[#dfe6ec] to-transparent"></div>
     </div>
 
-    <!-- Input bar -->
+    <!-- Footer link -->
     <a
       href="https://t.me/scientia_ru"
       target="_blank"
       rel="noopener"
-      class="flex items-center gap-3 border-t border-slate-200/80 bg-white px-4 py-2.5 transition-colors hover:bg-slate-50"
+      class="group flex items-center justify-center gap-2 bg-[#2AABEE] px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-[#229ED9]"
     >
-      <span class="flex-1 rounded-full bg-slate-100 px-4 py-2 text-sm text-slate-400">
-        Написать сообщение…
-      </span>
-      <svg class="h-6 w-6 shrink-0 text-[#2AABEE]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
-        <path d="M21.9 4.6c.2-1.2-.9-2.1-2-1.7L2.7 9.6c-1.2.5-1.1 2.2.1 2.6l4.4 1.4 1.7 5.4c.3 1.1 1.7 1.4 2.4.5l2.4-2.9 4.5 3.3c1 .7 2.4.2 2.6-1L21.9 4.6z" />
+      Перейти в телеграм
+      <svg class="h-4 w-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
+        <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12l-7.5 7.5M21 12H3" />
       </svg>
     </a>
   </div>
@@ -98,6 +106,13 @@ const fmt = new Intl.DateTimeFormat("ru-RU", { day: "numeric", month: "long", ho
     clip-path: path("M12 0 C 6 2, 0 8, 0 16 L 12 16 Z");
   }
 
+  /* Telegram-style expandable quote inside posts (injected via set:html — needs :global) */
+  .tg-bubble :global(blockquote) {
+    margin: 4px 0;
+    padding: 2px 0 2px 10px;
+    border-left: 3px solid #2aabee;
+  }
+
   /* Entrance (transform only — opacity animations get stuck translucent in some compositors) */
   .tg-window {
     animation: tg-rise 0.45s ease-out both;
@@ -108,3 +123,13 @@ const fmt = new Intl.DateTimeFormat("ru-RU", { day: "numeric", month: "long", ho
     }
   }
 </style>
+
+<script>
+  // "Показать ещё": reveal the next chunk of hidden bubbles
+  const more = document.querySelector("[data-tg-more]");
+  more?.addEventListener("click", () => {
+    const hidden = [...document.querySelectorAll("#tg-widget .tg-bubble.hidden")];
+    hidden.slice(0, 8).forEach((el) => el.classList.remove("hidden"));
+    if (hidden.length <= 8) more.remove();
+  });
+</script>

Fișier diff suprimat deoarece este prea mare
+ 1 - 0
src/data/telegram-news.json


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff