memorium/islands/Search.tsx

190 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-08-06 00:33:06 +02:00
import { useEffect, useRef, useState } from "preact/hooks";
import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts";
2023-08-06 17:47:26 +02:00
import { IconGhost, IconLoader2, IconSearch } from "@components/icons.tsx";
2023-08-06 00:33:06 +02:00
import { useEventListener } from "@lib/hooks/useEventListener.ts";
2023-08-06 17:47:26 +02:00
import { SearchResult } from "@lib/types.ts";
import { resources } from "@lib/resources.ts";
import { isLocalImage } from "@lib/string.ts";
2023-08-08 11:18:54 +02:00
import { IS_BROWSER } from "$fresh/runtime.ts";
2023-08-08 21:50:23 +02:00
import Checkbox from "@components/Checkbox.tsx";
import { Rating } from "@components/Rating.tsx";
import { useSignal } from "@preact/signals";
2023-08-08 21:55:10 +02:00
import Image from "@components/Image.tsx";
2023-08-06 00:33:06 +02:00
export const RedirectSearchHandler = () => {
useEventListener("keydown", (e: KeyboardEvent) => {
if (e?.target?.nodeName == "INPUT") return;
if (
e.key === "?" &&
window.location.search === ""
) {
window.location.href += "?q=";
}
2023-08-08 11:18:54 +02:00
}, IS_BROWSER ? document?.body : undefined);
2023-08-06 00:33:06 +02:00
return <></>;
};
2023-08-06 17:47:26 +02:00
const SearchResultImage = ({ src }: { src: string }) => {
const imageSrc = isLocalImage(src)
? `/api/images?image=${src}&width=50&height=50`
: src;
return (
2023-08-08 21:55:10 +02:00
<Image
2023-08-06 17:47:26 +02:00
class="object-cover w-12 h-12 rounded-full"
src={imageSrc}
alt="preview image"
/>
);
};
export const SearchResultItem = (
{ item, showEmoji = false }: {
item: NonNullable<SearchResult["hits"]>[number];
showEmoji?: boolean;
},
) => {
const doc = item.document;
const resourceType = resources[doc.type];
const href = (resourceType) ? `${resourceType.link}/${doc.id}` : "";
return (
<a
href={href}
class="p-2 text-white flex gap-4 items-center rounded-2xl hover:bg-gray-700"
>
{doc?.image && <SearchResultImage src={doc.image} />}
{`${showEmoji && resourceType ? resourceType.emoji : ""}`} {doc?.name}
</a>
);
};
export const SearchResultList = (
{ result, showEmoji }: { result: SearchResult; showEmoji?: boolean },
) => {
return (
<div class="mt-4">
{result?.hits
? (
<div class="flex flex-col gap-4">
{result.hits.map((hit) => (
<SearchResultItem item={hit} showEmoji={showEmoji} />
))}
</div>
)
: <div style={{ color: "#818181" }}>No Results</div>}
</div>
);
};
2023-08-08 21:55:10 +02:00
const Search = (
2023-08-06 00:33:06 +02:00
{ q, type }: { q: string; type?: string },
) => {
const [searchQuery, setSearchQuery] = useState(q);
2023-08-06 17:47:26 +02:00
const [data, setData] = useState<SearchResult>();
2023-08-06 00:33:06 +02:00
const [isLoading, setIsLoading] = useState(false);
2023-08-08 21:50:23 +02:00
const showSeenStatus = useSignal(false);
2023-08-06 00:33:06 +02:00
const inputRef = useRef<HTMLInputElement>(null);
if ("history" in globalThis) {
const u = new URL(window.location.href);
if (u.searchParams.get("q") !== searchQuery) {
u.searchParams.set("q", searchQuery);
}
2023-08-08 21:50:23 +02:00
if (showSeenStatus.value) {
u.searchParams.set("status", "not-seen");
} else {
u.searchParams.delete("status");
}
window.history.replaceState({}, "", u);
2023-08-06 00:33:06 +02:00
}
2023-08-08 21:50:23 +02:00
console.log({ showSeen: showSeenStatus.value });
2023-08-06 00:33:06 +02:00
const fetchData = async (query: string) => {
try {
setIsLoading(true);
const fetchUrl = new URL(window.location.href);
fetchUrl.pathname = "/api/resources";
2023-08-06 17:47:26 +02:00
if (query) {
fetchUrl.searchParams.set("q", encodeURIComponent(query));
2023-08-06 00:33:06 +02:00
} else {
return;
}
2023-08-08 21:50:23 +02:00
if (showSeenStatus.value) {
fetchUrl.searchParams.set("status", "not-seen");
}
2023-08-06 00:33:06 +02:00
if (type) {
fetchUrl.searchParams.set("type", type);
}
const response = await fetch(fetchUrl);
const jsonData = await response.json();
setData(jsonData);
setIsLoading(false);
} catch (error) {
console.error("Error fetching data:", error);
setIsLoading(false);
}
};
useEffect(() => {
if (inputRef.current && searchQuery?.length === 0) {
inputRef.current?.focus();
}
}, [inputRef.current, searchQuery]);
const debouncedFetchData = useDebouncedCallback(fetchData, 500); // Debounce the fetchData function with a delay of 500ms
const handleInputChange = (event: Event) => {
const target = event.target as HTMLInputElement;
2023-08-06 17:47:26 +02:00
if (target.value !== searchQuery) {
setSearchQuery(target.value);
debouncedFetchData(target.value); // Call the debounced fetch function with the updated search query
}
2023-08-06 00:33:06 +02:00
};
2023-08-06 17:47:26 +02:00
useEffect(() => {
debouncedFetchData(q);
}, []);
2023-08-06 00:33:06 +02:00
return (
2023-08-06 17:47:26 +02:00
<div class="mt-2">
2023-08-08 21:50:23 +02:00
<header class="flex items-center gap-4">
<div
class="flex items-center gap-1 rounded-xl w-full shadow-2xl"
style={{ background: "#2B2930", color: "#818181" }}
>
{isLoading && searchQuery
? <IconLoader2 class="w-4 h-4 ml-4 mr-2 animate-spin" />
: <IconSearch class="w-4 h-4 ml-4 mr-2" />}
<input
type="text"
style={{ fontSize: "1.2em" }}
class="bg-transparent py-3 w-full"
ref={inputRef}
value={searchQuery}
onInput={handleInputChange}
/>
</div>
<Checkbox label="seen" checked={showSeenStatus} />
<Rating rating={4} />
</header>
2023-08-06 17:47:26 +02:00
{data?.hits?.length && !isLoading
? <SearchResultList showEmoji={!type} result={data} />
: isLoading
? <div />
: (
<div
class="flex items-center gap-2 p-2 my-4 mx-3"
style={{ color: "#818181" }}
>
<IconGhost class="animate-hover" />
No Results
</div>
)}
2023-08-06 00:33:06 +02:00
</div>
);
};
2023-08-08 21:55:10 +02:00
export default Search;