diff options
| author | Factiven <[email protected]> | 2023-12-24 13:03:54 +0700 |
|---|---|---|
| committer | Factiven <[email protected]> | 2023-12-24 13:03:54 +0700 |
| commit | 50a0f0240d7fef133eb5acc1bea2b1168b08e9db (patch) | |
| tree | 307e09e505580415a58d64b5fc3580e9235869f1 /pages/id | |
| parent | Update README.md (#104) (diff) | |
| download | moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.tar.xz moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.zip | |
migrate to typescript
Diffstat (limited to 'pages/id')
| -rw-r--r-- | pages/id/index.tsx (renamed from pages/id/index.js) | 4 | ||||
| -rw-r--r-- | pages/id/manga/[...id].tsx | 159 | ||||
| -rw-r--r-- | pages/id/manga/read/[...id].tsx | 87 | ||||
| -rw-r--r-- | pages/id/novel/[...id].tsx | 121 | ||||
| -rw-r--r-- | pages/id/novel/read/index.tsx | 115 | ||||
| -rw-r--r-- | pages/id/search.tsx | 221 |
6 files changed, 705 insertions, 2 deletions
diff --git a/pages/id/index.js b/pages/id/index.tsx index 5ef870d..9af2d06 100644 --- a/pages/id/index.js +++ b/pages/id/index.tsx @@ -3,7 +3,7 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; import Footer from "@/components/shared/footer"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import MobileNav from "@/components/shared/MobileNav"; export default function Home() { @@ -16,7 +16,7 @@ export default function Home() { <link rel="icon" href="/svg/c.svg" /> </Head> <main className="flex flex-col h-screen"> - <NewNavbar /> + <Navbar /> <MobileNav hideProfile /> {/* Create an under construction page with tailwind css */} <div className="h-full w-screen flex-center flex-grow flex-col"> diff --git a/pages/id/manga/[...id].tsx b/pages/id/manga/[...id].tsx new file mode 100644 index 0000000..513001e --- /dev/null +++ b/pages/id/manga/[...id].tsx @@ -0,0 +1,159 @@ +import axios from "axios"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Navbar } from "../../../components/shared/NavBar"; +import MobileNav from "../../../components/shared/MobileNav"; +import pls from "@/utils/request"; + +export interface DataType { + id: string; + title: string; + description: string; + image: string; + chapters: ChapterType[]; +} + +export interface ChapterType { + id: string; + title: string; + rilis: string; +} + +interface InfoNovelProps { + id: string; + API: string; +} + +export default function InfoNovel({ id, API }: InfoNovelProps) { + const [data, setData] = useState<DataType | null>(null); + const [loading, setLoading] = useState<boolean>(true); + + const [filter, setFilter] = useState<string>(""); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const data = await pls.get(`${API}/api/manga/info/` + id); + setData(data); + } catch (error) { + setData(null); + } finally { + setLoading(false); + } + } + fetchData(); + + return () => { + setData(null); + }; + }, [id]); + + const fuzzySearch = (text: string, query: string): boolean => { + const textLower = text.toLowerCase().replace(/\.|\s/g, ""); + const queryLower = query.toLowerCase().replace(/\.|\s/g, ""); + + let i = 0; + let j = 0; + + while (i < textLower.length && j < queryLower.length) { + if (textLower[i] === queryLower[j]) { + j++; + } + i++; + } + + return j === queryLower.length; + }; + + const filteredData = data?.chapters?.filter((chapter: ChapterType) => + fuzzySearch(chapter.title, filter) + ); + + return ( + <div className="flex flex-col items-center"> + <Navbar withNav paddingY="" scrollP={0} /> + <MobileNav hideProfile /> + <div className="relative w-full max-w-screen-lg mx-5 mt-5 px-5 lg:px-0 lg:mt-14"> + {data && ( + <div className="flex lg:flex-row flex-col z-30 pt-24 lg:px-5"> + <div className="shrink-0 z-50 w-[170px] h-[240px] rounded overflow-hidden bg-secondary/20"> + {data?.image && ( + <Image + src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( + data?.image + )}${`&headers=${encodeURIComponent( + JSON.stringify({ Referer: "https://komikindo.tv/" }) + )}`}`} + width={200} + height={200} + alt="coverImage" + className="z-50 w-[170px] h-[240px] object-cover" + /> + )} + </div> + <div className="flex flex-col items-start justify-end gap-2 lg:pl-5 z-30 mt-5 lg:mt-0"> + <h1 className="font-bold text-2xl lg:text-3xl font-outfit line-clamp-2"> + {data?.title} + </h1> + {/* <div className="flex gap-5 w-full"> + <p className="flex gap-2 font-bold font-karla"> + Format: <span>{data?.format}</span> + </p> + <p className="flex gap-2 font-bold font-karla"> + Release: <span>{data?.year}</span> + </p> + <p className="flex gap-2 font-bold font-karla"> + Status: <span>{data?.status}</span> + </p> + </div> */} + <p className="line-clamp-2 font-light font-karla"> + {data?.description} + </p> + </div> + </div> + )} + + <div className="mt-10"> + <input + className="appearance-none rounded bg-secondary px-2 py-1 font-karla outline-none" + placeholder="Search..." + value={filter} + onChange={(e) => setFilter(e.target.value)} + /> + </div> + + <div className="mt-5 flex flex-col gap-3"> + {filteredData?.map((chapter: ChapterType) => ( + <Link + key={chapter?.id} + href={`/id/manga/read/${id}/${chapter?.id}`} + className="py-3 bg-secondary w-full px-5 rounded" + > + <div className="flex justify-between items-center font-karla w-full"> + <div className=""> + <p className="font-bold">{chapter?.title}</p> + </div> + <p className="font-light">{chapter?.rilis}</p> + </div> + </Link> + ))} + </div> + <div className="w-full bg-secondary rounded-xl h-[200px] absolute inset-0 z-10" /> + </div> + </div> + ); +} + +export async function getServerSideProps({ params }: any) { + const { id } = params; + const API = process.env.ID_API; + // console.log(id); + return { + props: { + id, + API, + }, + }; +} diff --git a/pages/id/manga/read/[...id].tsx b/pages/id/manga/read/[...id].tsx new file mode 100644 index 0000000..4978e36 --- /dev/null +++ b/pages/id/manga/read/[...id].tsx @@ -0,0 +1,87 @@ +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { Navbar } from "@/components/shared/NavBar"; +import MobileNav from "@/components/shared/MobileNav"; +import pls from "@/utils/request"; + +type DataType = { + id: string; + title: string; + pages: PageType[]; +}; + +type PageType = { + index: string; + src: string; +}; + +interface ReadNovelProps { + mangaId: string; + chapterId: string; + API: string; +} + +export default function ReadNovel({ mangaId, chapterId, API }: ReadNovelProps) { + const [data, setData] = useState<DataType | null>(); + const [hideNav, setHideNav] = useState(false); + + useEffect(() => { + async function fetchData() { + if (chapterId) { + const data = await pls.get(`${API}/api/manga/pages/${chapterId}`); + setData(data); + } + } + fetchData(); + + return () => { + setData(null); + }; + }, [chapterId]); + + return ( + <div className="w-screen flex flex-col items-center"> + {!hideNav && ( + <> + <Navbar paddingY="2" scrollP={0} /> + <MobileNav hideProfile /> + </> + )} + <div className="block mt-12" onClick={() => setHideNav((prev) => !prev)}> + <div className="w-full h-full max-w-screen-lg pointer-events-none select-none"> + {data?.pages?.map((i) => ( + <div key={i.index}> + <Image + src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( + i.src + )}${`&headers=${encodeURIComponent( + JSON.stringify({ Referer: "https://komikindo.tv/" }) + )}`}`} + alt="image" + width={500} + height={500} + className="w-full h-full" + /> + </div> + ))} + </div> + </div> + </div> + ); +} + +export async function getServerSideProps({ params }: any) { + const { id } = params; + + const [mangaId, chapterId] = id; + + const API = process.env.ID_API; + + return { + props: { + mangaId, + chapterId, + API, + }, + }; +} diff --git a/pages/id/novel/[...id].tsx b/pages/id/novel/[...id].tsx new file mode 100644 index 0000000..7e9e155 --- /dev/null +++ b/pages/id/novel/[...id].tsx @@ -0,0 +1,121 @@ +import axios from "axios"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { Navbar } from "../../../components/shared/NavBar"; +import MobileNav from "../../../components/shared/MobileNav"; +import { GetServerSideProps } from "next"; + +type InfoNovelProps = { + id: string; + API: string; +}; + +type NovelData = { + image?: string; + title?: string; + Release?: string; + Status?: string; + Author?: string; + description?: string; + chapters?: { + chapterId?: string; + chapter?: string; + release?: string; + }[]; + notFound?: boolean; +}; + +export default function InfoNovel({ id, API }: InfoNovelProps) { + const [data, setData] = useState<NovelData>(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const { data } = await axios.get(`${API}/api/novel/info/` + id); + setData(data); + } catch (error) { + setData({ + notFound: true, + }); + } finally { + setLoading(false); + } + } + fetchData(); + + return () => { + setData(undefined); + }; + }, [id]); + + return ( + <div className="flex flex-col items-center"> + <Navbar withNav paddingY="" scrollP={0} /> + <MobileNav hideProfile /> + <div className="relative w-full max-w-screen-lg mx-5 mt-5 px-5 lg:px-0 lg:mt-14"> + {data && ( + <div className="flex lg:flex-row flex-col z-30 pt-24 lg:px-5"> + {data?.image && ( + <Image + src={data?.image} + width={200} + height={200} + alt="coverImage" + className="z-50 w-[170px] h-[240px] object-cover rounded" + /> + )} + <div className="flex flex-col items-start justify-end gap-2 lg:pl-5 z-30 mt-5 lg:mt-0"> + <h1 className="font-bold text-2xl lg:text-3xl font-outfit line-clamp-2"> + {data?.title} + </h1> + <div className="flex gap-5 w-full"> + <p className="flex gap-2 font-bold font-karla"> + Release: <span>{data?.Release}</span> + </p> + <p className="flex gap-2 font-bold font-karla"> + Status: <span>{data?.Status}</span> + </p> + <p className="flex-1 gap-2 font-bold font-karla overflow-x-hidden text-ellipsis whitespace-nowrap"> + Author: <span>{data?.Author}</span> + </p> + </div> + <p className="line-clamp-2 font-light font-karla"> + {data?.description} + </p> + </div> + </div> + )} + + <div className="mt-10 flex flex-col gap-3"> + {data?.chapters?.map((chapter) => ( + <Link + key={chapter?.chapterId} + href={`/id/novel/read/?id=${chapter?.chapterId}`} + className="py-3 bg-secondary w-full px-5 rounded" + > + <div className="flex justify-between w-full"> + <p className="font-bold font-karla">{chapter?.chapter}</p> + <p className="font-light font-karla">{chapter?.release}</p> + </div> + </Link> + ))} + </div> + <div className="w-full bg-secondary rounded-xl h-[200px] absolute inset-0 z-10" /> + </div> + </div> + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + const { id } = params || {}; + const API = process.env.ID_API; + return { + props: { + id, + API, + }, + }; +}; diff --git a/pages/id/novel/read/index.tsx b/pages/id/novel/read/index.tsx new file mode 100644 index 0000000..5f36e54 --- /dev/null +++ b/pages/id/novel/read/index.tsx @@ -0,0 +1,115 @@ +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Navbar } from "@/components/shared/NavBar"; +import MobileNav from "@/components/shared/MobileNav"; +import pls from "@/utils/request/index"; + +interface IData { + novelTitle: string; + title: string; + navigation: { + next: string; + prev: string; + }; + content: string; +} + +export async function getServerSideProps() { + const API = process.env.ID_API; + return { + props: { + API, + }, + }; +} + +export default function ReadNovel({ API }: { API: string }) { + const [data, setData] = useState<IData>(); + + const searchParams = useSearchParams(); + const id = searchParams.get("id"); + const mangaId = id?.split("/")[0]; + + useEffect(() => { + async function fetchData() { + if (id) { + const data = await pls.get(`${API}/api/novel/chapter/${id}`); + setData(data); + } + } + fetchData(); + + return () => { + setData(undefined); + }; + }, [id]); + + return ( + <> + <Navbar withNav paddingY="py-2" scrollP={2} /> + <MobileNav hideProfile /> + <div className="w-screen flex flex-col items-center"> + {/* {data && ( */} + <div className="flex items-center gap-5 w-full max-w-screen-lg px-5 mt-16 font-karla font-bold"> + <div className="flex gap-2"> + <Link + href={`/id/novel/read/?id=${data?.navigation?.prev}`} + className={`${ + data?.navigation?.prev ? "" : "pointer-events-none opacity-60" + } py-1 px-2 bg-secondary rounded`} + > + prev + </Link> + <Link + href={`/id/novel/read/?id=${data?.navigation?.next}`} + className={`${ + data?.navigation?.next ? "" : "pointer-events-none opacity-60" + } py-1 px-2 bg-secondary rounded`} + > + next + </Link> + </div> + <span>/</span> + <Link href={`/id/novel/${mangaId}`} className="text-lg line-clamp-1"> + {data?.novelTitle} + </Link> + </div> + {/* )} */} + <div className="block mt-5"> + <div className="px-5 w-full h-full max-w-screen-lg pointer-events-none select-none"> + <p className="text-xl font-bold my-5">{data?.title}</p> + {data?.content && ( + <p + dangerouslySetInnerHTML={{ __html: data?.content }} + className="space-y-5" + /> + )} + </div> + </div> + {data?.content && ( + <div className="px-5 py-10 w-full h-full max-w-screen-lg"> + <div className="flex w-full gap-2"> + <Link + href={`/id/novel/read/?id=${data?.navigation?.prev}`} + className={`${ + data?.navigation?.prev ? "" : "pointer-events-none opacity-60" + } py-1 px-2 bg-secondary rounded`} + > + prev + </Link> + <Link + href={`/id/novel/read/?id=${data?.navigation?.next}`} + className={`${ + data?.navigation?.next ? "" : "pointer-events-none opacity-60" + } py-1 px-2 bg-secondary rounded`} + > + next + </Link> + </div> + </div> + )} + </div> + </> + ); +} diff --git a/pages/id/search.tsx b/pages/id/search.tsx new file mode 100644 index 0000000..aa53fcd --- /dev/null +++ b/pages/id/search.tsx @@ -0,0 +1,221 @@ +import Image from "next/image"; +import { Fragment, useEffect, useState } from "react"; +import { + CheckIcon, + ChevronDownIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/outline"; +import Link from "next/link"; +import { Combobox, Transition } from "@headlessui/react"; +import pls from "@/utils/request"; + +const types = [ + { + name: "Novel", + value: "novel", + }, + { + name: "Manga", + value: "manga", + }, +]; + +type DataType = { + id: string; + title: string; + img: string; + synonym?: string; + status?: string; + genres?: string; + release?: string; +}; + +export async function getServerSideProps() { + const API = process.env.ID_API; + return { + props: { + API, + }, + }; +} + +export default function Search({ API }: { API: string }) { + const [data, setData] = useState<DataType[] | null>([]); + const [query, setQuery] = useState("a"); + + const [type, setType] = useState(types[0]); + + const handleQuery = async (e: any) => { + e.preventDefault(); + setData([]); + + try { + const data = await pls.get(`${API}/api/${type.value}/search/${query}`); + setData(data); + } catch (error) { + setData(null); + } + }; + + useEffect(() => { + async function fetchData() { + try { + const data = await pls.get(`${API}/api/${type.value}/search/${query}`); + setData(data); + } catch (error) { + setData(null); + } + } + fetchData(); + return () => { + setData(null); + }; + }, [type?.value]); + + useEffect(() => { + // run handleQuery when pressing enter + const handleEnter = (e: any) => { + if (e.key === "Enter") { + handleQuery(e); + } + }; + window.addEventListener("keydown", handleEnter); + + return () => { + window.removeEventListener("keydown", handleEnter); + }; + }, [query, type?.value]); + + const handleChange = (e: any) => { + setType(e); + setData(null); + }; + + return ( + <div className="flex flex-col items-center"> + <div className="w-full max-w-screen-lg px-5"> + <div className="flex justify-between mt-16"> + <div className="flex-1 max-w-[20%] items-center justify-end text-lg relative"> + <Combobox value={type} onChange={(e) => handleChange(e)}> + <Combobox.Button className="h-full w-full gap-5 py-[2px] bg-secondary/70 rounded text-sm font-karla flex items-center justify-between px-2"> + {type.name} + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </Combobox.Button> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Combobox.Options + className="absolute z-[55] mt-1 max-h-60 w-full rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={{ scrollbarGutter: "stable" }} + > + {types.length === 0 && query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + types.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 mx-2 rounded-md ${ + active ? "bg-white/5 text-white" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <Fragment> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </Fragment> + )} + </Combobox.Option> + )) + )} + </Combobox.Options> + </Transition> + </Combobox> + </div> + <form + onSubmit={handleQuery} + className="flex items-center justify-end relative space-x-2" + > + <input + type="text" + value={query} + onChange={(e) => setQuery(e.target.value)} + className="bg-secondary h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none" + /> + <button type="submit" className="text-white"> + <MagnifyingGlassIcon className="h-6 w-6 text-white" /> + </button> + </form> + </div> + <div className="mt-5 grid xxs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 gap-5 gap-y-5"> + {data !== null + ? data?.map((x, index) => ( + <div key={x.id + index} className="flex flex-col gap-2"> + <Link + href={`/id/${type.value}/${x.id}`} + className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded" + style={{ + paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%) + }} + > + {x.img && ( + <Image + src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( + x.img + )}${`&headers=${encodeURIComponent( + JSON.stringify({ Referer: "https://komikindo.tv/" }) + )}`}`} + alt={x.title} + sizes="(min-width: 808px) 50vw, 100vw" + quality={100} + fill + className="object-cover" + /> + )} + </Link> + <div> + <h1 className="line-clamp-2 font-karla font-bold"> + {x.title} + </h1> + </div> + </div> + )) + : "No results found"} + </div> + </div> + </div> + ); +} |