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

179 lines
6.3 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, props.comic.id]);
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={result.url}
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);
}