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
+
+
+
+
+ {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
+
+ |
|