PageSourceSearch

https://www.crnasia.com/blocks/article-cards/article-cards.js

js crnasia.com collected 2026-10-03 22:34:14 UTC 12,238 bytes, 317 lines download raw bytes

1/* eslint-disable import/extensions */
2/* eslint-disable no-restricted-syntax */
3/* eslint-disable no-console */
4import ffetch from '../../scripts/ffetch.js';
5import {
6  extractElementsFromString,
7  formatingDate,
8  estimateTime,
9  updateHostname,
10  formatToISO8601, // ADDED: import the ISO converter
11  shouldShowAnzeige,
12  createAnzeigeFlag,
13} from '../../scripts/shared.js';
14import { resolveArticleGatingFlags, shouldBeGated } from '../../scripts/authentication/helpers.js';
15import { createCategorySlug } from '../../scripts/url-utils.js';
16import { fetchPlaceholders, getMetadata } from '../../scripts/aem.js';
17
18/**
19 * Creates a title element for the article with a corresponding link
20 * @param {object} article The article object
21 * @param {Object<string, {gated: string|null, forcegate: boolean}>} [metadataCache]
22 * @returns {Promise<HTMLElement>} The title element with a link
23 */
24async function createArticleTitle(article, metadataCache = {}) {
25  // article-query-index omits gated/forcegate; resolve from the article page so
26  // gated=off articles do not inherit the listing page's gated=on metadata.
27  const { articleGated, forceGate } = await resolveArticleGatingFlags(article, metadataCache);
28  const isLock = shouldBeGated(
29    article.publisheddate,
30    article.type,
31    forceGate,
32    articleGated,
33    article.path
34  );
35
36  const articleTitle = document.createElement('div');
37  articleTitle.className = `article-title ${isLock ? 'lock' : ''}`;
38
39  // create an anchor tag for the title
40  const articleLink = document.createElement('a');
41  articleLink.href = article.path;
42  articleLink.setAttribute('aria-label', `Read more about: ${article.title}`);
43  articleLink.textContent = article.title;
44  articleTitle.appendChild(articleLink);
45
46  return articleTitle; // ensure we return the node
47}   
48
49/**
50 * Creates a published date element for the article
51 * @param {string} date The UNIX timestamp of the article's published date
52 * @param {number} wordcount The word count used for reading time estimation
53 * @returns {HTMLElement} The published date element
54 */
55function createPublishedDate(date, wordcount) {
56  // MINIMAL CHANGE: Convert date to ISO for display formatting.
57  const publishedDate = document.createElement('div');
58  publishedDate.className = 'article-date';
59  publishedDate.innerHTML = `
60      <img class="article-clock" alt="pub date" src="/icons/clock.svg"/>
61      ${formatingDate(formatToISO8601(date))}${estimateTime(wordcount)}
62  `;
63  return publishedDate;
64}
65
66/**
67 * Creates a category element for the article
68 * @param {object} article The article object
69 * @returns {HTMLElement} The category element with a link
70 */
71function createArticleCategory(article) {
72  const articleCategory = document.createElement('div');
73  articleCategory.className = 'article-category';
74  const firstCategory = extractElementsFromString(article.category)[0];
75  const articleCategoryLink = document.createElement('a');
76  articleCategoryLink.href = `/category/${createCategorySlug(firstCategory)}`;
77  articleCategoryLink.textContent = firstCategory;
78  articleCategoryLink.setAttribute('aria-label', `Read more stories on ${firstCategory}`);
79  articleCategory.appendChild(articleCategoryLink);
80  return articleCategory;
81}
82
83/**
84 * Generates an article card with the article's title, author, image,
85 * published date, and description
86 * @param {object} article The article object
87 * @param {HTMLElement} [container] Optional container to append to (defaults to .article-cards-wrapper > div)
88 */
89function getArticlesContainer() {
90  return document.querySelector('.article-cards-wrapper > div');
91}
92
93async function generateCard(article, container, metadataCache = {}) {
94  const target = container || getArticlesContainer();
95  if (!target) return null;
96  const articleCard = document.createElement('div');
97  articleCard.className = 'article-card';
98  if (shouldShowAnzeige(article)) {
99    articleCard.classList.add('anzeige-card');
100  }
101  const contextWrapper = document.createElement('div');
102  contextWrapper.className = 'article-context-wrapper';
103  if (shouldShowAnzeige(article)) {
104    contextWrapper.appendChild(createAnzeigeFlag('inline'));
105  }
106  contextWrapper.appendChild(createArticleCategory(article));
107  contextWrapper.appendChild(await createArticleTitle(article, metadataCache));
108  contextWrapper.appendChild(createPublishedDate(article.publisheddate, article.wordcount));
109  articleCard.appendChild(contextWrapper);
110  return { articleCard, target };
111}
112
113function appendGeneratedCard(generated) {
114  if (!generated?.articleCard || !generated?.target) return;
115  generated.target.appendChild(generated.articleCard);
116  updateHostname(generated.target);
117}
118
119/** Homepage only: stored so we can adjust visible card count after ad/side-reg load */
120const homepageArticleCardsState = { articles: [], container: null, metadataCache: {} };
121
122/**
123 * Sets how many article cards are visible on the homepage (10 when logged in, 16 for 250px ad, 18 for 650px ad).
124 * Call from delayed.js after right-ad is fitted and side-reg presence is known.
125 * @param {number} count Desired number of cards (10, 16, or 18)
126 */
127export async function setHomepageArticleCardCount(count) {
128  const { articles, container, metadataCache } = homepageArticleCardsState;
129  if (!articles.length || !container) return;
130  const cards = container.querySelectorAll('.article-card');
131  const current = cards.length;
132  const max = Math.min(count, articles.length);
133  if (max > current) {
134    const generated = await Promise.all(
135      articles.slice(current, max).map((article) => generateCard(article, container, metadataCache))
136    );
137    generated.forEach(appendGeneratedCard);
138  } else if (max < current) {
139    for (let i = current - 1; i >= max; i -= 1) {
140      cards[i].remove();
141    }
142  }
143}
144
145async function appendArticlesToPage(articles, maxCount = 10) {
146  const container = getArticlesContainer();
147  if (!container) return;
148  const isHomepage = getMetadata('template')?.toLowerCase() === 'homepage';
149  const metadataCache = homepageArticleCardsState.metadataCache || {};
150  if (isHomepage) {
151    homepageArticleCardsState.articles = articles;
152    homepageArticleCardsState.container = container;
153    homepageArticleCardsState.metadataCache = metadataCache;
154  }
155  const generated = await Promise.all(
156    articles.slice(0, maxCount).map((item) => generateCard(item, container, metadataCache))
157  );
158  generated.forEach(appendGeneratedCard);
159}
160
161/**
162 * In allArticlesByPath and allArticlesGenerator, update the publisheddate using formatToISO8601.
163 */
164async function allArticlesByPath(generator, articlePaths) {
165  // First, collect all articles from the generator into a map for efficient lookup
166  const articlesMap = new Map();
167  for await (const item of generator) {
168    if (item.publisheddate) {
169      item.publisheddate = formatToISO8601(item.publisheddate);
170    }
171    // Store articles by their pathname for quick lookup
172    const itemPathname = new URL(item.path, window.location.origin).pathname;
173    articlesMap.set(itemPathname, item);
174  }
175  
176  // Now build the result array in the same order as articlePaths
177  const result = [];
178  for (const path of articlePaths) {
179    const article = articlesMap.get(path);
180    if (article) {
181      result.push(article);
182    }
183  }
184  return result;
185}
186
187async function allArticlesGenerator(generator, topStoriesArticles) {
188  const result = [];
189  const { ignore } = await fetchPlaceholders();
190  const ignoreArray = ignore ? ignore.split(', ') : [];
191  
192  for await (const item of generator) {
193    if (item.publisheddate) {
194      item.publisheddate = formatToISO8601(item.publisheddate);
195    }
196    let isTopStorie = false;
197    for (const art of topStoriesArticles) {
198      if (art.title === item.title) {
199        isTopStorie = true;
200        break;
201      }
202    }
203    if (item.publisheddate && !isTopStorie && !ignoreArray.includes(item.type.toLowerCase()) && !ignoreArray.includes(item.category.toLowerCase())) {
204      result.push(item);
205    }
206  }
207  return result;
208}
209
210async function fetchArticles(url, hasArticlePaths, articlePaths, topStoriesArticles) {
211  try {
212    let articlesGenerator;
213    let articles;
214    const template = getMetadata('template');
215    const isNotHomepage = template && template.toLowerCase() !== 'homepage';
216    
217    if (hasArticlePaths) {
218      articlesGenerator = ffetch(url);
219      articles = await allArticlesByPath(articlesGenerator, articlePaths);
220      
221      // If not homepage and we have fewer than 10 articles, fill with latest news
222      if (isNotHomepage && articles.length < 10) {
223        // Fetch all articles (excluding top stories and ignored types/categories)
224        const allArticlesGen = ffetch(url).limit(120);
225        const allArticles = await allArticlesGenerator(allArticlesGen, topStoriesArticles);
226        
227        // Get pathnames of already displayed articles for exclusion
228        const existingPathnames = new Set(
229          articles.map(article => new URL(article.path, window.location.origin).pathname)
230        );
231        
232        // Filter out already displayed articles
233        const additionalArticles = allArticles.filter(article => {
234          const articlePathname = new URL(article.path, window.location.origin).pathname;
235          return !existingPathnames.has(articlePathname);
236        });
237        
238        // Sort by publish date (most recent first)
239        additionalArticles.sort((a, b) => new Date(b.publisheddate) - new Date(a.publisheddate));
240        
241        // Add articles up to 10 total (preserve order: manually added first, then latest news)
242        const remainingSlots = 10 - articles.length;
243        articles = articles.concat(additionalArticles.slice(0, remainingSlots));
244      }
245      // For manually added articles, preserve their order (don't sort)
246    } else {
247      articlesGenerator = ffetch(url).limit(120);
248      articles = await allArticlesGenerator(articlesGenerator, topStoriesArticles);
249      // Sort articles by publish date (most recent first).
250      articles.sort((a, b) => new Date(b.publisheddate) - new Date(a.publisheddate));
251    }
252    await appendArticlesToPage(articles);
253  } catch (error) {
254    console.error('Error fetching articles', error);
255  }
256}
257
258function getArticlesPaths(li) {
259  const arrayCollection = li;
260  const paths = [];
261  for (const item of arrayCollection) {
262    const path = new URL(item.querySelector('a').title);
263    paths.push(path.pathname);
264  }
265  return paths;
266}
267
268function getTopStoriesArticles() {
269  const topStoriesFirstArticle = document.querySelector('.content-section .top-stories .first-wrap .text-wrap');
270  const topStoriesFirstArticleTitle = topStoriesFirstArticle?.querySelector('.top-stories-title a');
271  const topStoriesFirstArticleAuthor = topStoriesFirstArticle?.querySelector(
272    '.bottom-wrap .top-stories-author a:nth-child(2)'
273  );
274
275  const topStoriesSecoundArticles = document.querySelectorAll(
276    '.content-section .top-stories .second-wrap .top-stories'
277  );
278
279  const topStoriesArticles = [];
280  const topStoriesFirstObj = {};
281
282  topStoriesFirstObj.title = topStoriesFirstArticleTitle?.textContent;
283  topStoriesFirstObj.author = topStoriesFirstArticleAuthor?.textContent;
284
285  topStoriesArticles.push(topStoriesFirstObj);
286
287  topStoriesSecoundArticles?.forEach((article) => {
288    const topStoriesSecoundArticleTitle = article.querySelector(
289      '.text-wrap .title-category-wrapper .top-stories-title a'
290    );
291    const topStoriesSecoundArticleAuthor = article.querySelector('.bottom-wrap .top-stories-author a:nth-child(2)');
292    const topStoriesObj = {};
293
294    topStoriesObj.title = topStoriesSecoundArticleTitle?.textContent;
295    topStoriesObj.author = topStoriesSecoundArticleAuthor?.textContent;
296
297    topStoriesArticles.push(topStoriesObj);
298  });
299
300  return topStoriesArticles;
301}
302
303export default function decorate(block) {
304  const topStoriesArticles = getTopStoriesArticles();
305  const ul = block.querySelector('div div ul');
306  let hasArticlePaths = false;
307  let articlePaths = [];
308
309  if (ul) {
310    const li = ul.getElementsByTagName('li');
311    articlePaths = getArticlesPaths(li);
312    hasArticlePaths = true;
313  }
314  // fetch and display articles
315  fetchArticles('/article-query-index.json', hasArticlePaths, articlePaths, topStoriesArticles);
316  block.querySelector('div').remove();
317}

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.