chore: search cleanup

This commit is contained in:
Cory Dransfeldt 2024-10-19 15:18:43 -07:00
parent e5f86e2a1f
commit 3da23da73d
No known key found for this signature in database
2 changed files with 101 additions and 92 deletions

View file

@ -61,3 +61,4 @@
/blog/digital-privacy-tools /posts/2021/digital-privacy-tools/ 301 /blog/digital-privacy-tools /posts/2021/digital-privacy-tools/ 301
/posts/2023/now-page-update-matter-favorites/ /posts/2023/now-page-update-favorite-articles-from-matter/ 301 /posts/2023/now-page-update-matter-favorites/ /posts/2023/now-page-update-favorite-articles-from-matter/ 301
/posts/2023/now-playing-eleventy-netlify-edge-functions-emoji/ /posts/2023/displaying-now-playing-data-with-matching-emoji-using-netlify-edge-functions-and-eleventy/ 301 /posts/2023/now-playing-eleventy-netlify-edge-functions-emoji/ /posts/2023/displaying-now-playing-data-with-matching-emoji-using-netlify-edge-functions-and-eleventy/ 301
/posts/2014/sublime-text-ctrl-tab-key-bindings/ /posts/2014/sublime-text-3-ctrl-tab-key-bindings/ 301

View file

@ -87,35 +87,39 @@ window.addEventListener('load', () => {
// search logic // search logic
;(() => { ;(() => {
if (!MiniSearch) return if (!MiniSearch || !document.querySelector('.search__form')) return
const miniSearch = new MiniSearch({
fields: ['title', 'description', 'tags', 'type'],
idField: 'id',
storeFields: ['id', 'title', 'url', 'description', 'type', 'tags', 'total_plays'],
searchOptions: {
boost: { title: 2, tags: 1.5 },
prefix: true,
fuzzy: 0.3,
},
})
const miniSearch = new MiniSearch({ fields: ['title', 'description', 'tags', 'type'] })
const $form = document.querySelector('.search__form') const $form = document.querySelector('.search__form')
const $input = document.querySelector('.search__form--input') const $input = document.querySelector('.search__form--input')
const $fallback = document.querySelector('.search__form--fallback')
const $typeCheckboxes = document.querySelectorAll('.search__form--type input[type="checkbox"]') const $typeCheckboxes = document.querySelectorAll('.search__form--type input[type="checkbox"]')
const $results = document.querySelector('.search__results') const $results = document.querySelector('.search__results')
const $loadMoreButton = document.querySelector('.search__load-more') const $loadMoreButton = document.querySelector('.search__load-more')
$form.removeAttribute('action')
$form.removeAttribute('method')
$fallback.remove()
const PAGE_SIZE = 10 const PAGE_SIZE = 10
let currentPage = 1 let currentPage = 1
let currentResults = [] let currentResults = []
let totalResults = 0
let resultsById = {} let resultsById = {}
let debounceTimeout let debounceTimeout
const parseMarkdown = markdown => { const parseMarkdown = (markdown = '') =>
if (!markdown) return '' markdown
return markdown
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>') .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
.replace(/\n/g, '<br>') .replace(/\n/g, '<br>')
.replace(/[#*_~`]/g, '') .replace(/[#*_~`]/g, '')
}
const truncateDescription = (markdown, maxLength = 150) => { const truncateDescription = (markdown, maxLength = 150) => {
const htmlDescription = parseMarkdown(markdown) const htmlDescription = parseMarkdown(markdown)
@ -128,32 +132,13 @@ window.addEventListener('load', () => {
const formatArtistTitle = (title, totalPlays) => const formatArtistTitle = (title, totalPlays) =>
totalPlays > 0 ? `${title} <strong class="highlight-text">${totalPlays} plays</strong>` : title totalPlays > 0 ? `${title} <strong class="highlight-text">${totalPlays} plays</strong>` : title
const renderSearchResults = results => { const updateLoadMoreButton = () => {
if (results.length > 0) { const moreResultsToShow = currentPage * PAGE_SIZE < totalResults
$results.innerHTML = results.map(({ title, url, description, type, total_plays }) => { $loadMoreButton.style.display = moreResultsToShow ? 'block' : 'none'
const truncatedDesc = truncateDescription(description)
const formattedTitle = type === 'artist' && total_plays !== undefined
? formatArtistTitle(title, total_plays)
: title
return `
<li class="search__results--result">
<a href="${url}">
<h3>${formattedTitle}</h3>
</a>
<p>${truncatedDesc}</p>
</li>
`
}).join('')
$results.style.display = 'block'
} else {
$results.innerHTML = '<li class="search__results--no-results">No results found.</li>'
$results.style.display = 'block'
}
} }
const appendSearchResults = results => { const renderSearchResults = results => {
const newResults = results.map(({ title, url, description, type, total_plays }) => { const resultHTML = results.map(({ title, url, description, type, total_plays }) => {
const truncatedDesc = truncateDescription(description) const truncatedDesc = truncateDescription(description)
const formattedTitle = type === 'artist' && total_plays !== undefined const formattedTitle = type === 'artist' && total_plays !== undefined
? formatArtistTitle(title, total_plays) ? formatArtistTitle(title, total_plays)
@ -168,39 +153,67 @@ window.addEventListener('load', () => {
</li> </li>
` `
}).join('') }).join('')
$results.insertAdjacentHTML('beforeend', newResults)
$results.innerHTML = resultHTML || '<li class="search__results--no-results">No results found.</li>'
$results.style.display = 'block'
} }
const loadSearchIndex = async (query = '', types = []) => { const appendSearchResults = results => {
const resultHTML = results.map(({ title, url, description, type, total_plays }) => {
const truncatedDesc = truncateDescription(description)
const formattedTitle = type === 'artist' && total_plays !== undefined
? formatArtistTitle(title, total_plays)
: title
return `
<li class="search__results--result">
<a href="${url}"><h3>${formattedTitle}</h3></a>
<p>${truncatedDesc}</p>
</li>
`
}).join('')
$results.insertAdjacentHTML('beforeend', resultHTML)
}
const loadSearchIndex = async (query = '', types = [], page = 1) => {
const typeQuery = types.join(',') const typeQuery = types.join(',')
try { try {
const response = await fetch(`https://coryd.dev/api/search?q=${query}&type=${typeQuery}`) const response = await fetch(`http://localhost:8787/api/search?q=${query}&type=${typeQuery}&page=${page}&pageSize=${PAGE_SIZE}`)
const index = await response.json() const index = await response.json()
const results = index.results
if (!Array.isArray(results)) return {} const results = index.results || []
totalResults = index.total || 0
if (!Array.isArray(results)) throw new Error('Expected results to be an array')
resultsById = results.reduce((acc, item) => { resultsById = results.reduce((acc, item) => {
acc[item.id] = item acc[item.id] = item
return acc return acc
}, {}) }, {})
miniSearch.removeAll() if (page === 1) {
miniSearch.addAll(index) miniSearch.removeAll()
return resultsById miniSearch.addAll(results)
}
return results
} catch (error) { } catch (error) {
console.error('Error fetching search data:', error) console.error('Error fetching search data:', error)
return {} return []
} }
} }
const getSearchResults = query =>
miniSearch.search(query).map(({ id }) => resultsById[id])
.filter(result => getSelectedTypes().includes(result.type))
const getSelectedTypes = () => const getSelectedTypes = () =>
Array.from($typeCheckboxes) Array.from($typeCheckboxes)
.filter(checkbox => checkbox.checked) .filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value) .map(checkbox => checkbox.value)
$input.addEventListener('input', () => { const handleInputEvent = async () => {
const query = $input.value.trim() const query = $input.value.trim()
clearTimeout(debounceTimeout) clearTimeout(debounceTimeout)
@ -211,42 +224,37 @@ window.addEventListener('load', () => {
} }
debounceTimeout = setTimeout(async () => { debounceTimeout = setTimeout(async () => {
resultsById = await loadSearchIndex(query, getSelectedTypes()) const results = await loadSearchIndex(query, getSelectedTypes(), 1)
const results = getSearchResults(query)
currentResults = results currentResults = results
currentPage = 1 currentPage = 1
renderSearchResults(getResultsForPage(currentPage)) renderSearchResults(getResultsForPage(currentPage))
$loadMoreButton.style.display = results.length > PAGE_SIZE ? 'block' : 'none' updateLoadMoreButton()
}, 300) }, 300)
}) }
$typeCheckboxes.forEach(checkbox => { const handleCheckboxChange = async () => {
checkbox.addEventListener('change', async () => { const results = await loadSearchIndex($input.value.trim(), getSelectedTypes(), 1)
resultsById = await loadSearchIndex($input.value.trim(), getSelectedTypes()) currentResults = results
const results = getSearchResults($input.value.trim()) currentPage = 1
currentResults = results
currentPage = 1
renderSearchResults(getResultsForPage(currentPage)) renderSearchResults(getResultsForPage(currentPage))
$loadMoreButton.style.display = results.length > PAGE_SIZE ? 'block' : 'none' updateLoadMoreButton()
}) }
})
$loadMoreButton.addEventListener('click', () => { const handleLoadMoreClick = async () => {
currentPage++ currentPage++
const nextResults = getResultsForPage(currentPage) const nextResults = await loadSearchIndex($input.value.trim(), getSelectedTypes(), currentPage)
appendSearchResults(nextResults) appendSearchResults(nextResults)
if (currentPage * PAGE_SIZE >= currentResults.length) $loadMoreButton.style.display = 'none' updateLoadMoreButton()
}) }
const getSearchResults = query =>
miniSearch.search(query, { prefix: true, fuzzy: 0.2, boost: { title: 2 } })
.map(({ id }) => resultsById[id])
.filter(result => getSelectedTypes().includes(result.type))
const getResultsForPage = page => const getResultsForPage = page =>
currentResults.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE) currentResults.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
$input.addEventListener('input', handleInputEvent)
$typeCheckboxes.forEach(checkbox => checkbox.addEventListener('change', handleCheckboxChange))
$loadMoreButton.addEventListener('click', handleLoadMoreClick)
})() })()
}) })