PageSourceSearch

https://www.smalltalks.pro/js/blog.js

js smalltalks.pro collected 2026-09-30 11:06:39 UTC 5,159 bytes, 174 lines download raw bytes

1import { apiFetch, resolveMediaUrl } from "./api-client.js";
2
3const gridElement = document.querySelector("[data-blog-grid]");
4const tagsElement = document.querySelector("[data-blog-tags]");
5const statusElement = document.querySelector("[data-blog-status]");
6
7let activeTagSlug = "";
8let posts = [];
9let tags = [];
10
11const formatDate = (isoDate) => {
12  if (!isoDate) {
13    return "";
14  }
15  const dateValue = new Date(isoDate);
16  if (Number.isNaN(dateValue.getTime())) {
17    return isoDate;
18  }
19  return dateValue.toLocaleDateString("ru-RU", {
20    day: "numeric",
21    month: "long",
22    year: "numeric",
23  });
24};
25
26const stripHtml = (rawHtml) => {
27  const tempElement = document.createElement("div");
28  tempElement.innerHTML = String(rawHtml || "");
29  return (tempElement.textContent || "").replace(/\s+/g, " ").trim();
30};
31
32const renderTags = () => {
33  if (!tagsElement) {
34    return;
35  }
36
37  tagsElement.replaceChildren();
38
39  const allButton = document.createElement("button");
40  allButton.type = "button";
41  allButton.className = `blog-page__tag${activeTagSlug ? "" : " blog-page__tag--active"}`;
42  allButton.textContent = "Все";
43  allButton.addEventListener("click", () => {
44    activeTagSlug = "";
45    renderTags();
46    renderPosts();
47  });
48  tagsElement.append(allButton);
49
50  tags.forEach((tag) => {
51    const buttonElement = document.createElement("button");
52    buttonElement.type = "button";
53    buttonElement.className = `blog-page__tag${
54      activeTagSlug === tag.slug ? " blog-page__tag--active" : ""
55    }`;
56    buttonElement.textContent = tag.name;
57    buttonElement.addEventListener("click", () => {
58      activeTagSlug = tag.slug;
59      renderTags();
60      renderPosts();
61    });
62    tagsElement.append(buttonElement);
63  });
64};
65
66const renderPosts = () => {
67  if (!gridElement) {
68    return;
69  }
70
71  gridElement.replaceChildren();
72
73  const filteredPosts = activeTagSlug
74    ? posts.filter((post) => post.tags?.some((tag) => tag.slug === activeTagSlug))
75    : posts;
76
77  if (!filteredPosts.length) {
78    const emptyElement = document.createElement("p");
79    emptyElement.className = "blog-page__empty";
80    emptyElement.textContent = posts.length
81      ? "По выбранному тегу статей нет."
82      : "Скоро здесь появятся полезные статьи.";
83    gridElement.append(emptyElement);
84    return;
85  }
86
87  filteredPosts.forEach((post) => {
88    const cardLink = document.createElement("a");
89    cardLink.className = "blog-card";
90    cardLink.href = `/blog/${encodeURIComponent(post.slug)}/`;
91    cardLink.setAttribute("aria-label", post.title);
92
93    if (post.cover_url) {
94      const mediaElement = document.createElement("div");
95      mediaElement.className = "blog-card__media";
96      const imageElement = document.createElement("img");
97      imageElement.className = "blog-card__image";
98      imageElement.src = resolveMediaUrl(post.cover_url);
99      imageElement.alt = "";
100      imageElement.width = 640;
101      imageElement.height = 360;
102      imageElement.loading = "lazy";
103      imageElement.decoding = "async";
104      mediaElement.append(imageElement);
105      cardLink.append(mediaElement);
106    }
107
108    const bodyElement = document.createElement("div");
109    bodyElement.className = "blog-card__body";
110
111    const dateElement = document.createElement("p");
112    dateElement.className = "blog-card__date";
113    dateElement.textContent = formatDate(post.published_date);
114
115    const titleElement = document.createElement("h2");
116    titleElement.className = "blog-card__title";
117    titleElement.textContent = post.title;
118
119    const excerptElement = document.createElement("p");
120    excerptElement.className = "blog-card__date";
121    const excerptText = stripHtml(post.content).slice(0, 140);
122    excerptElement.textContent = excerptText ? `${excerptText}…` : "";
123
124    bodyElement.append(dateElement, titleElement);
125    if (excerptText) {
126      bodyElement.append(excerptElement);
127    }
128
129    if (post.tags?.length) {
130      const tagsWrap = document.createElement("div");
131      tagsWrap.className = "blog-card__tags";
132      post.tags.forEach((tag) => {
133        const tagElement = document.createElement("span");
134        tagElement.className = "blog-card__tag";
135        tagElement.textContent = tag.name;
136        tagsWrap.append(tagElement);
137      });
138      bodyElement.append(tagsWrap);
139    }
140
141    cardLink.append(bodyElement);
142    gridElement.append(cardLink);
143  });
144};
145
146const loadBlog = async () => {
147  if (statusElement) {
148    statusElement.textContent = "Загружаем статьи…";
149  }
150
151  const [postsResult, tagsResult] = await Promise.all([
152    apiFetch("/api/blog/posts/"),
153    apiFetch("/api/blog/tags/"),
154  ]);
155
156  if (!postsResult.response.ok) {
157    if (statusElement) {
158      statusElement.textContent = "Не удалось загрузить статьи. Попробуйте позже.";
159    }
160    return;
161  }
162
163  posts = postsResult.data?.results || [];
164  tags = tagsResult.response.ok ? tagsResult.data?.results || [] : [];
165
166  if (statusElement) {
167    statusElement.textContent = "";
168  }
169
170  renderTags();
171  renderPosts();
172};
173
174document.addEventListener("DOMContentLoaded", loadBlog);

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.