aboutsummaryrefslogtreecommitdiff
path: root/pages/id
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-12-24 13:03:54 +0700
committerFactiven <[email protected]>2023-12-24 13:03:54 +0700
commit50a0f0240d7fef133eb5acc1bea2b1168b08e9db (patch)
tree307e09e505580415a58d64b5fc3580e9235869f1 /pages/id
parentUpdate README.md (#104) (diff)
downloadmoopa-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].tsx159
-rw-r--r--pages/id/manga/read/[...id].tsx87
-rw-r--r--pages/id/novel/[...id].tsx121
-rw-r--r--pages/id/novel/read/index.tsx115
-rw-r--r--pages/id/search.tsx221
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>
+ );
+}