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.