chore: update deps
This commit is contained in:
parent
2c14869ed8
commit
0c728b1558
3 changed files with 193 additions and 17 deletions
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -38,7 +38,7 @@
|
||||||
"postcss-import-ext-glob": "^2.1.1",
|
"postcss-import-ext-glob": "^2.1.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"terser": "^5.34.1",
|
"terser": "^5.36.0",
|
||||||
"truncate-html": "^1.1.2"
|
"truncate-html": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -728,9 +728,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.12.1",
|
"version": "8.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
|
||||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -1066,9 +1066,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001668",
|
"version": "1.0.30001669",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
|
||||||
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
|
"integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1646,9 +1646,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.38",
|
"version": "1.5.39",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.38.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz",
|
||||||
"integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==",
|
"integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -2562,9 +2562,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/liquidjs": {
|
"node_modules/liquidjs": {
|
||||||
"version": "10.17.0",
|
"version": "10.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.18.0.tgz",
|
||||||
"integrity": "sha512-M4MC5/nencttIJHirl5jFTkl7Yu+grIDLn3Qgl7BPAD3BsbTCQknDxlG5VXWRwslWIjk8lSZZjVq9LioILDk1Q==",
|
"integrity": "sha512-gCJPmpmZ3oi2rMMHo/c+bW1LaRF+ZAKYTWQmKXPp0uK9EkWMFRmgbk3+Io4LSJGAOnpCZSgHJbNzcygx3kfAAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -4461,9 +4461,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.34.1",
|
"version": "5.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
|
||||||
"integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
|
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
"postcss-import-ext-glob": "^2.1.1",
|
"postcss-import-ext-glob": "^2.1.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"terser": "^5.34.1",
|
"terser": "^5.36.0",
|
||||||
"truncate-html": "^1.1.2"
|
"truncate-html": "^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
176
src/assets/scripts/search.js
Normal file
176
src/assets/scripts/search.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
;(() => {
|
||||||
|
if (!MiniSearch) return
|
||||||
|
const miniSearch = new MiniSearch({
|
||||||
|
fields: ['title', 'text', 'tags', 'type'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const $form = document.querySelector('.search__form')
|
||||||
|
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 $results = document.querySelector('.search__results')
|
||||||
|
const $loadMoreButton = document.querySelector('.search__load-more')
|
||||||
|
|
||||||
|
$form.removeAttribute('action')
|
||||||
|
$form.removeAttribute('method')
|
||||||
|
$fallback.remove()
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
let currentPage = 1
|
||||||
|
let currentResults = []
|
||||||
|
|
||||||
|
const loadSearchIndex = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/search')
|
||||||
|
const index = await response.json()
|
||||||
|
const resultsById = index.reduce((byId, result) => {
|
||||||
|
byId[result.id] = result
|
||||||
|
return byId
|
||||||
|
}, {})
|
||||||
|
miniSearch.addAll(index)
|
||||||
|
return resultsById
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching search index:', error)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultsById = {}
|
||||||
|
let debounceTimeout
|
||||||
|
|
||||||
|
loadSearchIndex().then(loadedResultsById => resultsById = loadedResultsById)
|
||||||
|
|
||||||
|
const getSelectedTypes = () => {
|
||||||
|
return Array.from($typeCheckboxes)
|
||||||
|
.filter(checkbox => checkbox.checked)
|
||||||
|
.map(checkbox => checkbox.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.addEventListener('input', () => {
|
||||||
|
const query = $input.value
|
||||||
|
|
||||||
|
clearTimeout(debounceTimeout)
|
||||||
|
|
||||||
|
if (query.length === 0) {
|
||||||
|
renderSearchResults([])
|
||||||
|
$loadMoreButton.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
const results = (query.length > 1) ? getSearchResults(query) : []
|
||||||
|
currentResults = results
|
||||||
|
currentPage = 1
|
||||||
|
|
||||||
|
renderSearchResults(getResultsForPage(currentPage))
|
||||||
|
$loadMoreButton.style.display = results.length > PAGE_SIZE ? 'block' : 'none'
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
$input.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter') event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
$typeCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
const query = $input.value
|
||||||
|
const results = getSearchResults(query)
|
||||||
|
currentResults = results
|
||||||
|
currentPage = 1
|
||||||
|
|
||||||
|
renderSearchResults(getResultsForPage(currentPage))
|
||||||
|
$loadMoreButton.style.display = results.length > PAGE_SIZE ? 'block' : 'none'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$loadMoreButton.addEventListener('click', () => {
|
||||||
|
currentPage++
|
||||||
|
const nextResults = getResultsForPage(currentPage)
|
||||||
|
appendSearchResults(nextResults)
|
||||||
|
|
||||||
|
if (currentPage * PAGE_SIZE >= currentResults.length) $loadMoreButton.style.display = 'none'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getSearchResults = (query) => {
|
||||||
|
const selectedTypes = getSelectedTypes()
|
||||||
|
|
||||||
|
return miniSearch.search(query, { prefix: true, fuzzy: 0.2, boost: { title: 2 } })
|
||||||
|
.map(({ id }) => resultsById[id])
|
||||||
|
.filter(result => selectedTypes.includes(result.type))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResultsForPage = (page) => {
|
||||||
|
const start = (page - 1) * PAGE_SIZE
|
||||||
|
const end = page * PAGE_SIZE
|
||||||
|
return currentResults.slice(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMarkdown = (markdown) => {
|
||||||
|
if (!markdown) return ''
|
||||||
|
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||||
|
markdown = markdown.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
|
||||||
|
markdown = markdown.replace(/\n/g, '<br>')
|
||||||
|
markdown = markdown.replace(/[#*_~`]/g, '')
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncateDescription = (markdown, maxLength = 150) => {
|
||||||
|
const htmlDescription = parseMarkdown(markdown)
|
||||||
|
const tempDiv = document.createElement('div')
|
||||||
|
tempDiv.innerHTML = htmlDescription
|
||||||
|
const plainText = tempDiv.textContent || tempDiv.innerText || ''
|
||||||
|
if (plainText.length > maxLength) return plainText.substring(0, maxLength) + '...'
|
||||||
|
return plainText
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatArtistTitle = (title, totalPlays) => {
|
||||||
|
if (totalPlays > 0) return `${title} <strong class="highlight-text">${totalPlays} plays</strong>`
|
||||||
|
return `${title}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSearchResults = (results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
$results.innerHTML = results.map(({ title, url, description, type, total_plays }) => {
|
||||||
|
const truncatedDesc = truncateDescription(description)
|
||||||
|
let formattedTitle = title
|
||||||
|
|
||||||
|
if (type === 'artist' && total_plays !== undefined) formattedTitle = formatArtistTitle(title, total_plays)
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="search__results--result">
|
||||||
|
<a href="${url}">
|
||||||
|
<h3>${formattedTitle}</h3>
|
||||||
|
</a>
|
||||||
|
<p>${truncatedDesc}</p>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
}).join('\n')
|
||||||
|
$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 newResults = results.map(({ title, url, description, type, total_plays }) => {
|
||||||
|
const truncatedDesc = truncateDescription(description)
|
||||||
|
let formattedTitle = title
|
||||||
|
|
||||||
|
if (type === 'artist' && total_plays !== undefined) formattedTitle = formatArtistTitle(title, total_plays)
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="search__results--result">
|
||||||
|
<a href="${url}">
|
||||||
|
<h3>${formattedTitle}</h3>
|
||||||
|
</a>
|
||||||
|
<p>${truncatedDesc}</p>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
}).join('\n')
|
||||||
|
$results.insertAdjacentHTML('beforeend', newResults)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
Reference in a new issue