137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
|
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);
|
||
|
}
|