Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
kailasnadh790 committed Apr 5, 2024
2 parents 693767c + 129102c commit a0e44ea
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 66 deletions.
27 changes: 24 additions & 3 deletions cigaradvisor/blocks/article-list/article-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import {
import { buildArticleTeaser } from '../article-teaser/article-teaser.js';
import { generatePagination, getCategory } from '../../scripts/util.js';

export async function renderPage(wrapper, articles, limit) {
/**
* Renders the page with the given wrapper element, articles, limit, and articlesCount.
*
* @param {HTMLElement} wrapper - The wrapper element to render the page into.
* @param {Array} articles - The array of articles to render.
* @param {number} limit - The limit of articles per page.
* @param {number} articlesCount - The total count of articles. This is passed for pagination
* when full list of articles are not passed.
* @returns {Promise<void>} - A promise that resolves when the page is rendered.
*/
export async function renderPage(wrapper, articles, limit, articlesCount) {
let pageSize = 10;
if (!articles || articles.length === 0) {
return;
Expand All @@ -23,14 +33,25 @@ export async function renderPage(wrapper, articles, limit) {
if (match) {
currentPage = Number.isNaN(parseInt(match[1], 10)) ? currentPage : parseInt(match[1], 10);
}
const totalPages = Math.ceil(articles.length / pageSize);
let totalPages;
let articleList;
/* articlesCount is passed when full list of articles are not passed.
* This is needed for pagination.
*/
if (articlesCount) {
totalPages = Math.ceil(articlesCount / pageSize);
articleList = [...articles];
} else {
totalPages = Math.ceil(articles.length / pageSize);
articleList = articles.slice((currentPage - 1) * pageSize, currentPage * pageSize);
}

// populating authors and categories info cache
await Promise.all([getAllAuthors(), fetchAllCategories()]).then();

// eslint-disable-next-line max-len
// eslint-disable-next-line max-len
const articlePromises = articles.slice((currentPage - 1) * pageSize, currentPage * pageSize).map(async (article) => {
const articlePromises = articleList.map(async (article) => {
const articleTeaser = document.createElement('div');
articleTeaser.classList.add('article-teaser');
articleTeaser.classList.add('block');
Expand Down
37 changes: 36 additions & 1 deletion cigaradvisor/blocks/search-results/search-results.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
.search-results.block p {
text-align: center;
}
}

.loading-image-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

.search-results.block .loading-image {
display: block;
margin: 0 auto;
width: 100vw;
height: 300px;
}

.search-results.block .loader {
width: 48px;
height: 48px;
border: 5px solid var(--medium-grey);
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}

@keyframes rotation {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}
211 changes: 149 additions & 62 deletions cigaradvisor/blocks/search-results/search-results.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,173 @@ import { renderPage } from '../article-list/article-list.js';

const searchParams = new URLSearchParams(window.location.search);

function compareFound(hit1, hit2) {
return hit1.minIdx - hit2.minIdx;
}

function filterData(searchTerms, data) {
const foundInHeader = [];
const foundInText = [];
const IGNORED_TERMS = [];

const doMatch = (property, term) => {
const regex = new RegExp(term, 'gi');
if (property) {
return property.match(regex);
}
return false;
};

function filterData(fullTerm, data) {
const searchTokens = [];
searchTokens.push(fullTerm);

searchTokens.push(...fullTerm.toLowerCase().split(/\s+/).filter((term) => term && term.length > 2 && term !== fullTerm.toLowerCase()));

// Object
// {
// priority: Number
// article: article
// count: Number
// }
const results = [];
data.forEach((result) => {
let minIdx = -1;

searchTerms.forEach((term) => {
// eslint-disable-next-line max-len
const idx = (result.heading || result.title || result.description).toLowerCase().indexOf(term);
if (idx < 0) return;
if (minIdx < idx) minIdx = idx;
const found = {
article: result,
count: 0,
};

searchTokens.forEach((token) => {
if (IGNORED_TERMS.includes(token.toLowerCase().trim())) return;
// eslint-disable-next-line no-param-reassign
if (token.endsWith('s')) token = token.substring(0, token.length - 1); // Handle potential pluralization of token.

if (doMatch(result.title, token)) {
found.rank ||= 1;
found.count += 1;
}
if (doMatch(result.heading, token)) {
found.rank ||= 2;
found.count += 1;
}
if (doMatch(result.description, token)) {
found.rank ||= 3;
found.count += 1;
}
if (doMatch(result.blurb, token)) {
found.rank ||= 4;
found.count += 1;
}
if (doMatch(result.text, token)) {
found.rank ||= 5;
found.count += 1;
}
});

if (minIdx >= 0) {
foundInHeader.push({ minIdx, result });
return;
if (found.count > 0) {
results.push(found);
}
});

const fullText = result.text ? result.text.toLowerCase() : '';
searchTerms.forEach((term) => {
const idx = fullText.indexOf(term);
if (idx < 0) return;
if (minIdx < idx) minIdx = idx;
});

if (minIdx >= 0) {
foundInText.push({ minIdx, result });
return results.sort((l, r) => {
if (l.rank < r.rank) {
return -1;
}
});
if (l.rank === r.rank) {
if (l.count > r.count) {
return -1;
}
if (l.count < r.count) {
return 1;
}
return 0;
}
return 1; // Left rank is greater than right rank - move it down the list.
}).map((r) => r.article);
}

return [
...foundInHeader.sort(compareFound),
...foundInText.sort(compareFound),
].map((item) => item.result);
/**
* Get details of each search result from the article-index.
*
* @param {Array} results - The search results.
* @param {Array} allArticles - All the articles.
* @param {HTMLElement} wrapper - The wrapper element to append the results.
* @param {number} limit - The maximum number of articles to display.
* @param {number} articlesCount - The total count of articles. This is needed for pagination.
* @returns {Promise<void>} - A promise that resolves when the page is rendered.
*/
async function processSearchResults(results, allArticles, wrapper, limit, articlesCount) {
const articles = [];
results.forEach((post) => {
const filteredArticles = allArticles.filter((obj) => obj.path === getRelativePath(post.path));
articles.push(filteredArticles[0]);
});
await renderPage(wrapper, articles, limit, articlesCount);
const loadingImageContainer = document.querySelector('.loading-image-container');
const articleListWrapper = document.querySelector('.article-list-wrapper');
articleListWrapper.style.transition = 'opacity 2s';
articleListWrapper.style.opacity = 1;
articleListWrapper.style.display = 'block';
loadingImageContainer.style.transition = 'opacity 2s';
loadingImageContainer.style.opacity = 0;
loadingImageContainer.style.display = 'none';
}

async function handleSearch(searchValue, wrapper, limit) {
async function handleSearch(searchValue, block, limit) {
const wrapper = block.querySelector('.article-teaser-wrapper');
const searchSummary = document.createElement('p');
searchSummary.classList.add('search-summary');
if (searchValue.length < 3 || !searchValue.match(/[a-z]/i)) {
searchSummary.innerHTML = 'Please enter at least three (3) characters to search.';
wrapper.prepend(searchSummary);
wrapper.replaceChildren(searchSummary);
return;
}
const searchTerms = searchValue.toLowerCase().split(/\s+/).filter((term) => !!term);
// show loading spinner
const loadingImageContainer = document.createElement('div');
loadingImageContainer.classList.add('loading-image-container');
const loadingImage = document.createElement('img');
loadingImage.classList.add('loading-image');
loadingImage.src = '/cigaradvisor/images/search/ca-search-results-loading.svg';
loadingImageContainer.append(loadingImage);
const spinner = document.createElement('span');
spinner.classList.add('loader');
loadingImageContainer.append(spinner);
block.prepend(loadingImageContainer);

const data = await getSearchIndexData();
const filteredData = filterData(searchTerms, data);
const filteredData = filterData(searchValue, data);
const articlesCount = filteredData.length;

const articles = [];
const allArticles = await loadPosts();
filteredData.forEach((post) => {
const filteredArticles = allArticles.filter((obj) => obj.path === getRelativePath(post.path));
articles.push(filteredArticles[0]);
});
searchSummary.textContent = `Your search for "${searchValue}" resulted in ${articles.length} articles`;
if (articles.length === 0) {

searchSummary.textContent = `Your search for "${searchValue}" resulted in ${filteredData.length} articles`;
if (filteredData.length === 0) {
const noResults = document.createElement('p');
noResults.classList.add('no-results');
noResults.textContent = 'Sorry, we couldn\'t find the information you requested!';
wrapper.replaceChildren(searchSummary);
wrapper.append(noResults);
} else {
await renderPage(wrapper, articles, limit);
loadingImageContainer.style.display = 'none';
return;
}
let filteredDataCopy = [...filteredData];

// load the first page of results
let resultsToShow = filteredDataCopy.slice(0, limit);
// eslint-disable-next-line max-len
await processSearchResults(resultsToShow, allArticles, wrapper, limit, articlesCount);

wrapper.prepend(searchSummary);

// handle pagination. Render each page of results when the hash changes
window.addEventListener('hashchange', async () => {
const heroSearch = document.querySelector('.hero-search');
if (heroSearch) {
heroSearch.querySelector('input').value = searchValue;
}
const url = new URL(window.location.href);
const hashParams = new URLSearchParams(url.hash.substring(1));
const page = hashParams.get('page');
const start = (page - 1) * limit;
const end = start + limit;
filteredDataCopy = [...filteredData];
resultsToShow = filteredDataCopy.slice(start, end);
await processSearchResults(resultsToShow, allArticles, wrapper, limit, articlesCount);
wrapper.prepend(searchSummary);
});
}

export default async function decorate(block) {
Expand All @@ -92,29 +190,18 @@ export default async function decorate(block) {
const articleTeaserWrapper = document.createElement('div');
articleTeaserWrapper.classList.add('article-teaser-wrapper');

if (searchParams.get('s')) {
const searchValue = searchParams.get('s');
const heroSearch = document.querySelector('.hero-search');
if (heroSearch) {
heroSearch.querySelector('input').value = searchValue;
}
await handleSearch(searchValue, articleTeaserWrapper, limit);
}

articleList.append(articleTeaserWrapper);

articleListWrapper.append(articleList);

block.replaceChildren(articleListWrapper);

window.addEventListener('hashchange', async () => {
if (searchParams.get('s')) {
const searchValue = searchParams.get('s');
const heroSearch = document.querySelector('.hero-search');
if (heroSearch) {
heroSearch.querySelector('input').value = searchValue;
}
await handleSearch(searchValue, articleTeaserWrapper, limit);
if (searchParams.get('s')) {
const searchValue = searchParams.get('s').trim();
const heroSearch = document.querySelector('.hero-search');
if (heroSearch) {
heroSearch.querySelector('input').value = searchValue;
}
});
handleSearch(searchValue, block, limit);
}
}
3 changes: 3 additions & 0 deletions helix-query.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ indices:
description:
select: head > meta[property="og:description"]
value: attribute(el, "content")
blurb:
select: head > meta[property="articleblurb"]
value: attribute(el, "content")
heading:
select: main h1:first-of-type
value: textContent(el)
Expand Down

0 comments on commit a0e44ea

Please sign in to comment.