comicbox/pages/comic/[comic]/search.tsx

137 lines
4.9 KiB
TypeScript
Raw Normal View History

2022-08-01 16:18:55 +00:00
import { Comic, ComicBubble, ComicPage } from "@prisma/client";
import { Hit, SearchResponse } from "meilisearch";
import { GetServerSideProps } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { ParsedUrlQuery } from "querystring";
import { useEffect, useState } from "react";
import { LinkButton } from "../../../components/Button";
import HighlightComic from "../../../components/HighlightComic";
import TextInput from "../../../components/TextInput";
import { prisma } from "../../../src/db";
import { searchClient } from "../../../src/search";
interface Params extends ParsedUrlQuery {
comic: string;
}
interface Props {
initialSearchQuery: string;
comic: Comic;
}
type ComicPageResult = { id: string; } & Omit<ComicPage, 'id'> & {
comic: Comic;
bubbles: ComicBubble[];
}
export default function Search(props: Props) {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState(props.initialSearchQuery);
const [results, setResults] = useState<SearchResponse<ComicPageResult>>();
const [selected, setSelected] = useState(-1);
useEffect(() => {
setSearchQuery((Array.isArray(router.query.search) ? router.query.search[0] : router.query.search) || '');
}, [router.query]);
useEffect(() => {
searchClient.index('comic_pages').search<ComicPageResult>(searchQuery, {
attributesToHighlight: ['*'],
filter: [
`comic.id=${props.comic.id}`,
],
}).then(results => {
setResults(results);
});
}, [searchQuery]);
return <main className='p-4'>
<Head>
<title>Search {props.comic.title}</title>
</Head>
<div className='w-full flex flex-col items-center justify-center'>
<TextInput multiline={false} value={searchQuery} onChange={(e) => {
router.replace({
pathname: `/comic/${props.comic.slug}/search`,
query: {
search: e.target.value
},
}, undefined, {
shallow: true,
});
setSearchQuery(e.target.value);
setSelected(-1);
}} placeholder='Type to search...' className='w-full max-w-xl' />
</div>
{results && <div className='flex flex-row w-full gap-4 justify-center pt-4'>
<section className={'max-w-1/2 ' + (selected === -1 && 'min-w-xl')}>
<p>
<i>~{results.estimatedTotalHits} total results</i>
</p>
<ul>
{results.hits.map((result, i) => {
return <li key={result.id}>
<div>
<h2>
<LinkButton href={{
pathname: `/comic/${props.comic.slug}/highlight/${result.id.split('-')[1]}`,
query: {
highlightedBubbles: findHighlighedBubbles(result),
},
}} target='_blank' onClick={(e) => {
e.preventDefault();
setSelected(i === selected ? -1 : i);
}}>
{result.title}
</LinkButton>
</h2>
</div>
</li>
})}
</ul>
</section>
{selected !== -1 && results.hits[selected] && <section className='max-w-1/2'>
<h2>{results.hits[selected].title}</h2>
<HighlightComic page={{
...results.hits[selected],
id: parseInt(results.hits[selected].id.split('-')[1]),
}} highlightedBubbles={findHighlighedBubbles(results.hits[selected])} />
</section>}
</div>}
</main>
}
export const getServerSideProps: GetServerSideProps<Props, Params> = async ({ params, query }) => {
const { comic: comicId } = params!;
const comic = await prisma.comic.findFirst({
where: {
slug: comicId,
},
});
if (!comic) return { notFound: true };
const search = (Array.isArray(query.search) ? query.search[0] : query.search) || '';
return {
props: {
initialSearchQuery: search,
comic,
},
}
}
function findHighlighedBubbles(result: Hit<ComicPageResult>) {
return result.bubbles.filter((bubble, i) => {
if (!result._formatted?.bubbles) throw new Error();
const fmt = result._formatted?.bubbles[i].content;
return fmt !== bubble.content;
}).map(bubble => bubble.idx);
}