diff --git a/components/HighlightComic.tsx b/components/HighlightComic.tsx new file mode 100644 index 0000000..df10b46 --- /dev/null +++ b/components/HighlightComic.tsx @@ -0,0 +1,44 @@ +import { Comic, ComicBubble, ComicPage } from "@prisma/client"; +import Image from "next/image" +import { percentify, randomColour } from "../src/utils"; + +interface Props { + page: ComicPage & { + bubbles: ComicBubble[]; + comic: Comic; + }; + highlightedBubbles: number[] | null; +} + +export default function HighlightComic(props: Props) { + return
+
+ +
+ {props.highlightedBubbles && props.highlightedBubbles.map(bubble => { + return
+ })} +
+} diff --git a/pages/comic/[comic]/highlight/[page].tsx b/pages/comic/[comic]/highlight/[page].tsx new file mode 100644 index 0000000..9bf04c9 --- /dev/null +++ b/pages/comic/[comic]/highlight/[page].tsx @@ -0,0 +1,63 @@ +import { Comic, ComicBubble, ComicPage } from "@prisma/client"; +import { GetServerSideProps } from "next"; +import Head from "next/head"; +import { ParsedUrlQuery } from "querystring"; +import HighlightComic from "../../../../components/HighlightComic"; +import { prisma } from "../../../../src/db"; + +interface Params extends ParsedUrlQuery { + comic: string; + page: string; +} + +interface Props { + page: ComicPage & { + bubbles: ComicBubble[]; + comic: Comic; + }; + highlightedBubbles: number[] | null; +} + +export default function Search(props: Props) { + return
+ + {props.page.title} - {props.page.comic.title} + + +
+} + +export const getServerSideProps: GetServerSideProps = async ({ + params, + query, +}) => { + const { comic: comicId, page: id } = params!; + const pageId = parseInt(id); + if (isNaN(pageId)) return { notFound: true }; + + const page = await prisma?.comicPage.findFirst({ + where: { + comic: { + slug: comicId, + }, + id: pageId, + }, + include: { + bubbles: true, + comic: true, + }, + }); + + if (!page) return { notFound: true }; + + const highlightedBubbles = (Array.isArray(query.highlightedBubbles) ? query.highlightedBubbles : (query.highlightedBubbles ? [query.highlightedBubbles] : undefined)) + ?.map((bubble) => parseInt(bubble)) + ?.filter((bubble) => !isNaN(bubble)); + + return { + props: { + page, + highlightedBubbles: highlightedBubbles ?? null, + } + } +} diff --git a/pages/comic/[comic]/search.tsx b/pages/comic/[comic]/search.tsx new file mode 100644 index 0000000..5c7fd70 --- /dev/null +++ b/pages/comic/[comic]/search.tsx @@ -0,0 +1,136 @@ +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 & { + comic: Comic; + bubbles: ComicBubble[]; +} + +export default function Search(props: Props) { + const router = useRouter(); + + const [searchQuery, setSearchQuery] = useState(props.initialSearchQuery); + const [results, setResults] = useState>(); + 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(searchQuery, { + attributesToHighlight: ['*'], + filter: [ + `comic.id=${props.comic.id}`, + ], + }).then(results => { + setResults(results); + }); + }, [searchQuery]); + + return
+ + Search {props.comic.title} + + +
+ { + 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' /> +
+ + {results &&
+
+

+ ~{results.estimatedTotalHits} total results +

+ +
    + {results.hits.map((result, i) => { + return
  • +
    +

    + { + e.preventDefault(); + setSelected(i === selected ? -1 : i); + }}> + {result.title} + +

    +
    +
  • + })} +
+
+ {selected !== -1 && results.hits[selected] &&
+

{results.hits[selected].title}

+ + +
} +
} +
+} + +export const getServerSideProps: GetServerSideProps = 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) { + 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); +} diff --git a/pages/index.tsx b/pages/index.tsx index ac0dd90..a76c152 100755 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -26,8 +26,9 @@ const Home = (props: Props) => { Comic + Search Completion - Links + Link @@ -39,6 +40,11 @@ const Home = (props: Props) => { {comic.title} + + + Search + +