2022-08-01 16:20:29 +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';
|
2022-08-01 16:18:55 +00:00
|
|
|
|
|
|
|
interface Params extends ParsedUrlQuery {
|
|
|
|
comic: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
initialSearchQuery: string;
|
|
|
|
comic: Comic;
|
|
|
|
}
|
|
|
|
|
2022-08-01 16:20:29 +00:00
|
|
|
type ComicPageResult = { id: string } & Omit<ComicPage, 'id'> & {
|
|
|
|
comic: Comic;
|
|
|
|
bubbles: ComicBubble[];
|
|
|
|
};
|
2022-08-01 16:18:55 +00:00
|
|
|
|
|
|
|
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(() => {
|
2022-08-01 16:20:29 +00:00
|
|
|
setSearchQuery(
|
|
|
|
(Array.isArray(router.query.search)
|
|
|
|
? router.query.search[0]
|
|
|
|
: router.query.search) || ''
|
|
|
|
);
|
2022-08-01 16:18:55 +00:00
|
|
|
}, [router.query]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2022-08-01 16:20:29 +00:00
|
|
|
searchClient
|
|
|
|
.index('comic_pages')
|
|
|
|
.search<ComicPageResult>(searchQuery, {
|
|
|
|
attributesToHighlight: ['*'],
|
|
|
|
filter: [`comic.id=${props.comic.id}`],
|
|
|
|
})
|
|
|
|
.then((results) => {
|
2022-08-01 16:18:55 +00:00
|
|
|
setResults(results);
|
|
|
|
});
|
|
|
|
}, [searchQuery]);
|
|
|
|
|
2022-08-01 16:20:29 +00:00
|
|
|
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>
|
|
|
|
);
|
2022-08-01 16:18:55 +00:00
|
|
|
}
|
|
|
|
|
2022-08-01 16:20:29 +00:00
|
|
|
export const getServerSideProps: GetServerSideProps<Props, Params> = async ({
|
|
|
|
params,
|
|
|
|
query,
|
|
|
|
}) => {
|
2022-08-01 16:18:55 +00:00
|
|
|
const { comic: comicId } = params!;
|
|
|
|
|
|
|
|
const comic = await prisma.comic.findFirst({
|
|
|
|
where: {
|
|
|
|
slug: comicId,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!comic) return { notFound: true };
|
|
|
|
|
2022-08-01 16:20:29 +00:00
|
|
|
const search =
|
|
|
|
(Array.isArray(query.search) ? query.search[0] : query.search) || '';
|
2022-08-01 16:18:55 +00:00
|
|
|
return {
|
|
|
|
props: {
|
|
|
|
initialSearchQuery: search,
|
|
|
|
comic,
|
|
|
|
},
|
2022-08-01 16:20:29 +00:00
|
|
|
};
|
|
|
|
};
|
2022-08-01 16:18:55 +00:00
|
|
|
|
|
|
|
function findHighlighedBubbles(result: Hit<ComicPageResult>) {
|
2022-08-01 16:20:29 +00:00
|
|
|
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);
|
2022-08-01 16:18:55 +00:00
|
|
|
}
|