-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSearchLogic.tsx
More file actions
199 lines (178 loc) · 5.87 KB
/
SearchLogic.tsx
File metadata and controls
199 lines (178 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import Fuse from "fuse.js";
// @ts-ignore
import DOMPurify from "dompurify";
import { createEffect, createSignal, onMount } from "solid-js";
export default function SearchLogic() {
const [searchQuery, setSearchQuery] = createSignal("");
const [searchResults, setSearchResults] = createSignal<ContentItem[]>([]);
interface ContentItem {
i: number;
c: string;
s: string;
t: string;
d: string;
}
interface WordMap {
[key: string]: number[];
}
interface SearchData {
wordMap: WordMap;
content: ContentItem[];
}
interface FuzzyData {
word: string;
id: number[];
}
let FUSE_SEARCH: Fuse<FuzzyData> | null = null;
let FUZZY_SEARCH_DATA: FuzzyData[];
let SEARCH_DATA: SearchData = {
wordMap: {},
content: [],
};
const options = {
keys: [{ name: "word"}],
includeScore: true,
threshold: 0.2
};
function getMapValue(m: Map<number, number>, k: number): number {
return m.get(k) || 0;
}
async function searchContent(query: string): Promise<ContentItem[]> {
if (!FUSE_SEARCH) {
FUSE_SEARCH = new Fuse(FUZZY_SEARCH_DATA, options);
}
const words = query.split(" ");
const idCount = new Map<number, number>();
for (const word of words) {
const matchedIds = SEARCH_DATA.wordMap[word.toLowerCase()];
if (matchedIds) {
matchedIds.forEach((id) =>
idCount.set(id, getMapValue(idCount, id) + 1)
);
} else {
const fuzzyResults = FUSE_SEARCH.search(word, { limit: 2 });
fuzzyResults.forEach((res) => {
res.item.id.forEach((id) => {
idCount.set(id, getMapValue(idCount, id) + (res?.score || 0));
});
});
}
}
const sortedIds = Array.from(idCount.entries()).sort((a, b) => b[1] - a[1]);
return sortedIds.map(([id]) => SEARCH_DATA.content[id]);
}
async function fetchSearchResults(
searchText: string
): Promise<ContentItem[]> {
try {
if (SEARCH_DATA.content.length === 0) {
const res = await fetch("/search.json");
if (!res.ok) return [];
SEARCH_DATA = await res.json();
FUZZY_SEARCH_DATA = Object.entries(SEARCH_DATA.wordMap).map(
([word, id]) => ({
word,
id,
})
);
}
if (searchText.length > 2) {
return await searchContent(searchText);
}
return [];
} catch (e) {
return [];
}
}
createEffect(async () => {
if (searchQuery().length < 2) {
setSearchResults([]);
} else {
setSearchResults(await fetchSearchResults(searchQuery()));
}
});
function updateSearchResults(queryText: string) {
const searchText = DOMPurify.sanitize(queryText);
setSearchQuery(searchText);
}
onMount(() => {
const params = new URLSearchParams(window.location.search);
const searchText = params.get("") || "";
if (searchText) {
updateSearchResults(searchText);
}
});
const onInput = (e: Event) => {
const target = e.target as HTMLInputElement;
updateSearchResults(target.value);
};
const onResultClick = (searchText: string) => {
const url = new URL(window.location.href);
url.searchParams.set("", searchText);
window.history.pushState({}, '', url);
};
return (
<div>
<div>
<input
id="search"
name="search"
type="search"
placeholder="What are you looking for?"
required
min="2"
max="48"
value={searchQuery()}
onInput={onInput}
class="w-full px-1.5 py-1 rounded outline-none text-black dark:text-white bg-slate-200/50 dark:bg-slate-400/15 border border-black/25 dark:border-white/30 focus:border-black focus:dark:border-white placeholder-gray-500 dark:placeholder-gray-300"
/>
</div>
<div>
<p class="flex flex-col mt-5">
{searchQuery().length === 0
? ""
: searchQuery().length > 0 && searchQuery().length < 3
? "Enter at least 3 letters for the search."
: `Search results for "${searchQuery()}"`}
</p>
<ul class="flex flex-col mt-6">
{searchQuery().length > 2 && searchResults().length === 0 ? (
<li>No results found.</li>
) : (
searchResults().map((result) => (
<li>
<a
href={`/${result.c === "b" ? "blog" : "projects"}/${result.s}`}
class="relative group flex flex-nowrap py-3 px-4 pr-10 mb-4 rounded-lg border border-black/35 text-gray-700 dark:text-gray-200 dark:border-white/20 hover:bg-slate-900/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out"
onClick={() => onResultClick(searchQuery())}
>
<div class="flex flex-col flex-1 truncate">
<div class="font-semibold">{result.t}</div>
<div class="text-sm">{result.d}</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute top-1/2 right-2 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
class="translate-x-3 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out"
/>
<polyline
points="12 5 19 12 12 19"
class="-translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out"
/>
</svg>
</a>
</li>
))
)}
</ul>
</div>
</div>
);
}