awesome-privacy/web/src/components/things/Search.svelte

216 lines
5.4 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import Fuse from 'fuse.js';
import { slugify } from '@utils/fetch-data';
import type { Category, Section, Service, ShortService } from '../../types/Service';
import { formatLink } from '@utils/parse-markdown';
import { prepareSearchItems, searchOptions } from '@utils/do-searchy-searchy';
export let data: Category[];
export let previousSearch: string | undefined = undefined;
let fuse: Fuse<any>;
let searchQuery = '';
let results: any[] = [];
// Initialize Fuse.js
onMount(() => {
const items = prepareSearchItems(data);
fuse = new Fuse(items, searchOptions);
});
const makeResultLink = (cat?: string, sec?: string, itm?: string) => {
if (!cat) return '/'
if (!sec) return `/${slugify(cat)}`
if (!itm) return `/${slugify(cat)}/${slugify(sec)}`
return `/${slugify(cat)}/${slugify(sec)}#${slugify(itm)}`;
};
const makeResultText = (cat?: string, sec?: string, itm?: string) => {
if (itm) return itm;
if (sec) return sec;
if (cat) return cat;
return '';
};
const makeLogoSrc = (logo: string, url: string) => {
if (!logo && !url) return '/broken-image.png';
return logo || `https://icon.horse/icon/${formatLink(url)}`;
};
const makeTitle = (typ: string, desc: string) => {
if (desc && typ === 'Service') {
return `${desc.slice(0, 60)}...`
}
return '';
};
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
if (window) {
window.location.href = `/search/${encodeURIComponent(searchQuery)}`;
}
}
if (event.key === 'Escape') {
searchQuery = '';
}
}
// Watch for changes in the search query and update results
$: if (searchQuery) {
results = fuse.search(searchQuery).map(result => result.item).splice(0, 25);
} else {
results = [];
}
</script>
<div class="search-wrap">
<label for="search">
What are you looking for?
{#if searchQuery.length > 0}
<span class="enter-hint">Press enter to view all results</span>
{/if}
</label>
<input
id="search"
placeholder={previousSearch || 'Start typing...'}
autocomplete="off"
bind:value={searchQuery}
on:keydown={handleKeyDown}
/>
{#if searchQuery.length > 0}
<div class="suggestions">
<ul>
{#each results as result}
<li class="result-row">
<a
href={makeResultLink(result.category, result.sectionName, result.name)}
title={makeTitle(result.type, result.description)}
>
<span class="name">
{#if result.type === 'Service'}
<img src={makeLogoSrc(result.logo, result.url)} alt={result.name} width="20" height="20" loading="lazy" />
{/if}
{makeResultText(result.category, result.sectionName, result.name)}
{#if result.itemCount}
<i>({result.itemCount})</i>
{/if}
</span>
<span class="path">
{result.category ? `${result.category}` : ''}
{result.sectionName ? ` ${result.sectionName}` : ''}
{result.name ? ` ${result.name}` : ''}
</span>
</a>
</li>
{/each}
</ul>
</div>
{/if}
</div>
<style lang="scss">
.search-wrap {
display: flex;
flex-direction: column;
position: relative;
margin: 1rem auto;
max-width: 900px;
margin: 0 auto;
width: 80vw;
label {
margin: 0.5rem 0;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
.enter-hint {
font-size:0.8rem;
opacity: 0.7;
}
}
input {
padding: 0.5rem 1rem;
font-size: 1.8rem;
border: 2px solid var(--foreground);
border-radius: var(--curve-lg);
box-shadow: 3px 3px 0 var(--foreground);
z-index: 4;
&:focus {
outline: none;
border-color: var(--accent);
box-shadow: 3px 3px 0 var(--accent);
}
}
.suggestions {
ul {
position: absolute;
background: white;
z-index: 3;
width: 100%;
list-style: none;
padding: 0;
margin: 0;
border: 2px solid var(--foreground);
border-radius: 0 0 var(--curve-lg) var(--curve-lg);
box-shadow: 3px 3px 0 var(--foreground);
transform: translateY(-0.5rem);
max-height: 500px;
overflow-y: scroll;
li.result-row {
padding: 0.5rem 1rem;
margin: 0.5rem 0;
a {
color: var(--foreground);
text-decoration: none;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.name {
display: flex;
align-items: center;
gap: 0.5rem;
i {
color: var(--accent);
font-weight: bold;
font-style: normal;
}
img {
border-radius: var(--curve-md);
width: 1.25rem;
height: 1.25rem;
font-size: 10px;
color: var(--accent);
overflow: hidden;
background: #f453974d;
padding: 1px;
}
}
.path {
font-size: 0.85rem;
opacity: 0.7;
}
}
&:hover {
background: var(--accent);
.name i {
color: var(--accent-fg);
}
}
}
}
}
}
</style>