Last active
February 22, 2022 03:21
-
-
Save zadeviggers/b52b968346fbb20b6f3f858576dc499d to your computer and use it in GitHub Desktop.
The code for my search bar (https://publictransportforum.nz/articles). Made in SolidJS (with FuseJS for fuzzy matching) to be used in an Astro site. Note that this code is ripped straight from my source so it might need a bit of tweaking to make it work for you.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Here are some snippets from the file that loads all my article's markdown files and does processing on them | |
// including generating the json file for search | |
import * as fs from "fs"; | |
import path from "path"; | |
const publicDirectory = path.join(process.cwd(), "public"); | |
// At the bottom of the function that populates the cache of articles... | |
const searchFile = JSON.stringify( | |
articles.map((article) => ({ | |
t: article.meta.unsafeTitle, | |
d: article.meta.unsafeDescription, | |
u: article.URL.linkHref, | |
ts: article.meta.tags.map((tag) => tag.formattedText).join(" "), | |
e: !!article.external, | |
})), | |
); | |
fs.writeFileSync(path.join(publicDirectory, "search.json"), searchFile); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Search bar component, made in SolidJS | |
// Make sure to add @withastro/renderer-solid to the renders key in your Astro config | |
// Also make sire to install solid-js and fuse.js | |
import { | |
For, | |
createEffect, | |
createResource, | |
createSignal, | |
onCleanup, | |
onMount, | |
} from "solid-js"; | |
import Fuse from "fuse.js"; | |
const ExternalLinkIconSVG = () => ( | |
<svg width="1em" height="1em" viewBox="0 0 24 24" class="external-link-icon"> | |
<g | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round"> | |
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> | |
<path d="M15 3h6v6"></path> | |
<path d="M10 14L21 3"></path> | |
</g> | |
</svg> | |
); | |
const highlightResult = ( | |
key: string, | |
text: string, | |
rawMatches?: { key: string; indices: [number, number][] }[], | |
): string => { | |
if (!rawMatches) return text; | |
let highlighted = []; | |
let matches = rawMatches.find((m) => m.key === key)?.indices || []; | |
let pair = matches.shift(); | |
for (let i = 0; i < text.length; i++) { | |
const char = text.charAt(i); | |
if (pair && i == pair[0]) { | |
highlighted.push("<strong>"); | |
} | |
highlighted.push(char); | |
if (pair && i == pair[1]) { | |
highlighted.push("</strong>"); | |
pair = matches.shift(); | |
} | |
} | |
return highlighted.join(""); | |
}; | |
export default function SearchBar() { | |
const [value, setValue] = createSignal(""); | |
const [wrapper, setWrapper] = createSignal<HTMLDivElement>(); | |
const [open, setOpen] = createSignal(false); | |
const [items] = createResource< | |
{ | |
t: string; | |
d: string; | |
u: string; | |
ts: string; | |
e: boolean; | |
}[] | |
>(async () => { | |
const res = await fetch("/search.json"); | |
const data = await res.json(); | |
return data; | |
}); | |
const fuse = () => | |
items.loading | |
? null | |
: new Fuse(items(), { | |
keys: [ | |
{ name: "t", weight: 0.8 }, | |
{ name: "ts", weight: 0.5 }, | |
{ name: "d", weight: 0.4 }, | |
], | |
includeMatches: true, | |
threshold: 0.7, | |
useExtendedSearch: true, | |
}); | |
// Close on click outside | |
const onDocumentClick = (e) => { | |
if (!wrapper().contains(e.currentTarget)) { | |
setOpen(false); | |
} | |
}; | |
onMount(() => document.addEventListener("click", onDocumentClick)); | |
onCleanup(() => document.removeEventListener("click", onDocumentClick)); | |
const onInput = (event) => { | |
setValue(event.target.value); | |
}; | |
createEffect(() => { | |
if (value().length > 0) { | |
setOpen(true); | |
} else { | |
setOpen(false); | |
} | |
}); | |
const searchResults = () => fuse()?.search(value()).slice(0, 11); | |
return ( | |
<div class="searchbar-wrapper" id="searchbar-wrapper" ref={setWrapper}> | |
<label for="searchbar" class="searchbar"> | |
Search:{" "} | |
<input | |
value={value()} | |
onInput={onInput} | |
type="text" | |
id="searchbar" | |
aria-expanded={open() ? "true" : "false"} | |
aria-controls="search-results" | |
/> | |
</label> | |
<div | |
id="search-results" | |
class="results" | |
style={`display: ${open() ? "block" : "none"};`}> | |
<ul> | |
<For | |
each={searchResults()} | |
fallback={ | |
<li> | |
<p>No results found </p> | |
</li> | |
} | |
children={(result) => ( | |
<li> | |
<a | |
href={result.item.u} | |
target={result.item.e ? "_blank" : null}> | |
{result.item.e && <ExternalLinkIconSVG />} | |
<span | |
innerHTML={highlightResult( | |
"t", | |
result.item.t, | |
result.matches as any, | |
)} | |
/> | |
</a> | |
</li> | |
)} | |
/> | |
</ul> | |
</div> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* The styles for the searchbar */ | |
/* --spacing is set to 8px on :root */ | |
.searchbar-wrapper { | |
margin-top: calc(var(--spacing) * 4); | |
--width: min(90vw, calc(var(--spacing) * 40)); | |
--wider-width: min(90vw, calc(var(--spacing) * 60)); | |
--highlight-colour: var(--rust); | |
--background-colour: var(--slate); | |
--text-colour: var(--white); | |
--hover-colour: var(--light-slate); | |
width: var(--width); | |
position: relative; | |
} | |
@media (prefers-color-scheme: dark) { | |
.searchbar-wrapper { | |
--highlight-colour: var(--blue); | |
--background-colour: var(--slate); | |
--text-colour: var(--white); | |
--hover-colour: var(--light-slate); | |
} | |
} | |
.searchbar-wrapper > .searchbar, | |
.searchbar-wrapper > .results { | |
background-color: var(--background-colour); | |
border-radius: var(--spacing); | |
padding-top: var(--spacing); | |
padding-bottom: var(--spacing); | |
color: var(--text-colour); | |
} | |
.searchbar-wrapper > .searchbar { | |
width: var(--width); | |
display: flex; | |
padding-left: var(--spacing); | |
padding-right: var(--spacing); | |
border: 2px solid transparent; | |
} | |
.searchbar-wrapper > .searchbar:focus-within { | |
border-color: var(--highlight-colour); | |
} | |
.searchbar-wrapper > .searchbar input { | |
margin-left: var(--spacing); | |
background-color: transparent; | |
color: var(--text-colour); | |
flex: 1; | |
} | |
.searchbar-wrapper > .searchbar input:focus { | |
outline: none; | |
} | |
.searchbar-wrapper > .results { | |
width: var(--wider-width); | |
overflow: hidden; | |
position: absolute; | |
top: calc(var(--spacing) * 6); | |
left: calc(-1 * calc(calc(var(--wider-width) - var(--width)) / 2)); | |
} | |
.searchbar-wrapper > .results > ul { | |
list-style-type: none; | |
display: flex; | |
flex-direction: column; | |
gap: var(--spacing); | |
} | |
.searchbar-wrapper > .results > ul > li { | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
} | |
.searchbar-wrapper > .results > ul > li::before { | |
display: none; | |
} | |
.searchbar-wrapper > .results > ul > li > * { | |
color: var(--text-colour); | |
display: flex; | |
align-items: center; | |
gap: var(--spacing); | |
padding: var(--spacing); | |
} | |
.searchbar-wrapper > .results > ul > li > a > .external-link-icon { | |
stroke: var(--text-colour); | |
stroke-width: 2px; | |
} | |
.searchbar-wrapper > .results > ul > li > a:hover, | |
.searchbar-wrapper > .results > ul > li > a:focus { | |
background-color: var(--hover-colour); | |
text-decoration: none; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment