diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js
deleted file mode 100644
index 7ef5de3..0000000
--- a/pages/en/profile/[user].js
+++ /dev/null
@@ -1,496 +0,0 @@
-import { getServerSession } from "next-auth";
-import { authOptions } from "../../api/auth/[...nextauth]";
-import Image from "next/image";
-import Link from "next/link";
-import Head from "next/head";
-import { useEffect, useState } from "react";
-import { getUser } from "@/prisma/user";
-import { NewNavbar } from "@/components/shared/NavBar";
-import { toast } from "sonner";
-
-export default function MyList({ media, sessions, user, time, userSettings }) {
- const [listFilter, setListFilter] = useState("all");
- const [visible, setVisible] = useState(false);
- const [useCustomList, setUseCustomList] = useState(true);
-
- useEffect(() => {
- if (userSettings) {
- localStorage.setItem("customList", userSettings.CustomLists);
- setUseCustomList(userSettings.CustomLists);
- }
- }, [userSettings]);
-
- // Function to handle checkbox state changes
- const handleCheckboxChange = async () => {
- setUseCustomList(!useCustomList); // Toggle the checkbox state
- try {
- const res = await fetch("/api/user/profile", {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- name: sessions?.user?.name,
- settings: {
- CustomLists: !useCustomList,
- },
- }),
- });
- const data = await res.json();
- if (data) {
- toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`);
- }
- localStorage.setItem("customList", !useCustomList);
- } catch (error) {
- console.error(error);
- }
- };
-
- const filterMedia = (status) => {
- if (status === "all") {
- return media;
- }
- return media.filter((m) => m.name === status);
- };
- return (
- <>
-
-
My Lists
-
-
-
-
-
-
-
- {user.bannerImage ? (
-
- ) : (
-
- )}
-
{user.name}
-
-
-
- Created At :
-
-
-
- {sessions && user.name === sessions?.user.name ? (
-
-
-
-
-
Edit Profile
-
- ) : null}
-
-
-
-
- {user.about ? (
-
- ) : (
- "No description created."
- )}
-
-
-
-
-
-
- {user.statistics.anime.episodesWatched}
-
- Total Episodes
-
-
-
- {user.statistics.anime.count}
-
- Total Anime
-
- {time?.days ? (
-
-
{time.days}
- Days Watched
-
- ) : (
-
-
{time.hours}
- hours
-
- )}
-
- {sessions && user.name === sessions?.user.name && (
-
-
User Settings
-
-
- Custom Lists
-
-
-
-
-
-
- )}
- {media.length !== 0 && (
-
-
-
-
setVisible(!visible)}
- >
-
-
-
-
-
-
-
- )}
-
-
-
- {media.length !== 0 ? (
- filterMedia(listFilter).map((item, index) => {
- return (
-
-
{item.name}
-
-
-
-
- Title
-
- Score
- Progress
-
-
-
- {item.entries.map((item) => {
- return (
-
-
-
- {item.media.status === "RELEASING" ? (
-
- ) : item.media.status === "NOT_YET_RELEASED" ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {item.media.title.romaji}
-
-
-
-
- {item.score === 0 ? null : item.score}
-
-
- {item.progress === item.media.episodes
- ? item.progress
- : item.media.episodes === null
- ? item.progress
- : `${item.progress}/${item.media.episodes}`}
-
-
- );
- })}
-
-
-
- );
- })
- ) : (
-
- {user.name === sessions?.user.name ? (
-
- Oops! Looks like you haven't watch anything yet.
-
- ) : (
-
- Oops! It looks like this user haven't watch anything
- yet.
-
- )}
-
-
-
-
-
Start Watching
-
-
- )}
-
-
- >
- );
-}
-
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
- const accessToken = session?.user?.token || null;
- const query = context.query;
-
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: `
- query ($username: String, $status: MediaListStatus) {
- MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) {
- user {
- id
- name
- about (asHtml: true)
- createdAt
- avatar {
- large
- }
- statistics {
- anime {
- count
- episodesWatched
- meanScore
- minutesWatched
- }
- }
- bannerImage
- mediaListOptions {
- animeList {
- sectionOrder
- }
- }
- }
- lists {
- status
- name
- entries {
- id
- mediaId
- status
- progress
- score
- media {
- id
- status
- title {
- english
- romaji
- }
- episodes
- coverImage {
- large
- }
- }
- }
- }
- }
- }
- `,
- variables: {
- username: query.user,
- },
- }),
- });
-
- const data = await response.json();
-
- const get = data.data.MediaListCollection;
- const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder;
-
- if (!sectionOrder) {
- return {
- notFound: true,
- };
- }
-
- let userData;
-
- if (session) {
- userData = await getUser(session.user.name, false);
- }
-
- const prog = get.lists;
-
- function getIndex(status) {
- const index = sectionOrder.indexOf(status);
- return index === -1 ? sectionOrder.length : index;
- }
-
- prog.sort((a, b) => getIndex(a.name) - getIndex(b.name));
-
- const user = get.user;
-
- const time = convertMinutesToDays(user.statistics.anime.minutesWatched);
-
- return {
- props: {
- media: prog,
- sessions: session,
- user: user,
- time: time,
- userSettings: userData?.setting || null,
- },
- };
-}
-
-function UnixTimeConverter({ unixTime }) {
- const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds
- const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD
-
- return
{formattedDate}
;
-}
-
-function convertMinutesToDays(minutes) {
- const hours = minutes / 60;
- const days = hours / 24;
-
- if (days >= 1) {
- return days % 1 === 0
- ? { days: `${parseInt(days)}` }
- : { days: `${days.toFixed(1)}` };
- } else {
- return hours % 1 === 0
- ? { hours: `${parseInt(hours)}` }
- : { hours: `${hours.toFixed(1)}` };
- }
-}
diff --git a/pages/en/profile/[user].tsx b/pages/en/profile/[user].tsx
new file mode 100644
index 0000000..82b88af
--- /dev/null
+++ b/pages/en/profile/[user].tsx
@@ -0,0 +1,509 @@
+import Image from "next/image";
+import Link from "next/link";
+import Head from "next/head";
+import { useEffect, useState } from "react";
+import { getUser } from "@/prisma/user";
+import { toast } from "sonner";
+import { Navbar } from "@/components/shared/NavBar";
+import pls from "@/utils/request";
+import { CurrentMediaTypes } from "..";
+
+type MyListProps = {
+ media: CurrentMediaTypes[];
+ sessions: any;
+ user: any;
+ time: any;
+ userSettings: any;
+};
+
+export default function MyList({
+ media,
+ sessions,
+ user,
+ time,
+ userSettings,
+}: MyListProps) {
+ const [listFilter, setListFilter] = useState("all");
+ const [visible, setVisible] = useState(false);
+ const [useCustomList, setUseCustomList] = useState(true);
+
+ useEffect(() => {
+ if (userSettings) {
+ localStorage.setItem("customList", userSettings.CustomLists);
+ setUseCustomList(userSettings.CustomLists);
+ }
+ }, [userSettings]);
+
+ // Function to handle checkbox state changes
+ const handleCheckboxChange = async () => {
+ setUseCustomList(!useCustomList); // Toggle the checkbox state
+ try {
+ const res = await fetch("/api/user/profile", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: sessions?.user?.name,
+ settings: {
+ CustomLists: !useCustomList,
+ },
+ }),
+ });
+ const data = await res.json();
+ if (data) {
+ toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`);
+ }
+ localStorage.setItem("customList", String(!useCustomList));
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const filterMedia = (status: string) => {
+ if (status === "all") {
+ return media;
+ }
+ return media.filter((m: { name: string }) => m.name === status);
+ };
+ return (
+ <>
+
+
My Lists
+
+
+
+
+
+
+
+
+ {user.bannerImage ? (
+
+ ) : (
+
+ )}
+
{user.name}
+
+
+
+ Created At :
+
+
+
+ {sessions && user.name === sessions?.user.name ? (
+
+
+
+
+
Edit Profile
+
+ ) : null}
+
+
+
+
+ {user.about ? (
+
+ ) : (
+ "No description created."
+ )}
+
+
+
+
+
+
+ {user.statistics.anime.episodesWatched}
+
+ Total Episodes
+
+
+
+ {user.statistics.anime.count}
+
+ Total Anime
+
+ {time?.days ? (
+
+
{time.days}
+ Days Watched
+
+ ) : (
+
+
{time.hours}
+ hours
+
+ )}
+
+ {sessions && user.name === sessions?.user.name && (
+
+
User Settings
+
+
+ Custom Lists
+
+
+
+
+
+
+ )}
+ {media.length !== 0 && (
+
+
+
+
setVisible(!visible)}
+ >
+
+
+
+
+
+
+
+ )}
+
+
+
+ {media.length !== 0 ? (
+ filterMedia(listFilter).map((item, index) => {
+ return (
+
+
{item.name}
+
+
+
+
+ Title
+
+ Score
+ Progress
+
+
+
+ {item.entries.map((item) => {
+ return (
+
+
+
+ {item.media.status === "RELEASING" ? (
+
+ ) : item.media.status === "NOT_YET_RELEASED" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {item.media.title.romaji}
+
+
+
+
+ {item.score === 0 ? null : item.score}
+
+
+ {item.progress === item.media.episodes
+ ? item.progress
+ : item.media.episodes === null
+ ? item.progress
+ : `${item.progress}/${item.media.episodes}`}
+
+
+ );
+ })}
+
+
+
+ );
+ })
+ ) : (
+
+ {user.name === sessions?.user.name ? (
+
+ Oops! Looks like you haven't watch anything yet.
+
+ ) : (
+
+ Oops! It looks like this user haven't watch anything
+ yet.
+
+ )}
+
+
+
+
+
Start Watching
+
+
+ )}
+
+
+ >
+ );
+}
+
+export async function getServerSideProps(context: any) {
+ const query = context.query;
+
+ const [data, session] = await pls.post(
+ "https://graphql.anilist.co/",
+ {
+ body: JSON.stringify({
+ query: `
+ query ($username: String, $status: MediaListStatus) {
+ MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) {
+ user {
+ id
+ name
+ about (asHtml: true)
+ createdAt
+ avatar {
+ large
+ }
+ statistics {
+ anime {
+ count
+ episodesWatched
+ meanScore
+ minutesWatched
+ }
+ }
+ bannerImage
+ mediaListOptions {
+ animeList {
+ sectionOrder
+ }
+ }
+ }
+ lists {
+ status
+ name
+ entries {
+ id
+ mediaId
+ status
+ progress
+ score
+ media {
+ id
+ status
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+ }
+ `,
+ variables: {
+ username: query.user,
+ },
+ }),
+ },
+ context
+ );
+
+ const get = data?.data?.MediaListCollection;
+ const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder;
+
+ if (!sectionOrder) {
+ return {
+ notFound: true,
+ };
+ }
+
+ let userData;
+
+ if (session) {
+ userData = await getUser(session.user.name, false);
+ }
+
+ const prog = get.lists;
+
+ function getIndex(status: string) {
+ const index = sectionOrder.indexOf(status);
+ return index === -1 ? sectionOrder.length : index;
+ }
+
+ prog.sort(
+ (a: { name: string }, b: { name: string }) =>
+ getIndex(a.name) - getIndex(b.name)
+ );
+
+ const user = get.user;
+
+ const time = convertMinutesToDays(user.statistics.anime.minutesWatched);
+
+ return {
+ props: {
+ media: prog,
+ sessions: session,
+ user: user,
+ time: time,
+ userSettings: userData?.setting || null,
+ },
+ };
+}
+
+function UnixTimeConverter({ unixTime }: { unixTime: number }) {
+ const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds
+ const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD
+
+ return
{formattedDate}
;
+}
+
+function convertMinutesToDays(minutes: number) {
+ const hours = minutes / 60;
+ const days = hours / 24;
+
+ if (days >= 1) {
+ return days % 1 === 0
+ ? { days: `${days}` }
+ : { days: `${days.toFixed(1)}` };
+ } else {
+ return hours % 1 === 0
+ ? { hours: `${hours}` }
+ : { hours: `${hours.toFixed(1)}` };
+ }
+}
diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.js
deleted file mode 100644
index f1e6730..0000000
--- a/pages/en/schedule/index.js
+++ /dev/null
@@ -1,485 +0,0 @@
-import Image from "next/image";
-import { useEffect, useRef, useState } from "react";
-import Link from "next/link";
-import { CalendarIcon } from "@heroicons/react/24/solid";
-import { ClockIcon } from "@heroicons/react/24/outline";
-import Loading from "@/components/shared/loading";
-import { timeStamptoAMPM, timeStamptoHour } from "@/utils/getTimes";
-import {
- filterFormattedSchedule,
- filterScheduleByDay,
- sortScheduleByDay,
- transformSchedule,
-} from "@/utils/schedulesUtils";
-
-import { scheduleQuery } from "@/lib/graphql/query";
-import MobileNav from "@/components/shared/MobileNav";
-
-import { useSession } from "next-auth/react";
-import { redis } from "@/lib/redis";
-import Head from "next/head";
-import { NewNavbar } from "@/components/shared/NavBar";
-
-const day = [
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- "Saturday",
-];
-
-const isAired = (timestamp) => {
- const currentTime = new Date().getTime() / 1000;
- return timestamp <= currentTime;
-};
-
-export async function getServerSideProps() {
- const now = new Date();
- // Adjust for Japan timezone (add 9 hours)
- const nowJapan = new Date(now.getTime() + 9 * 60 * 60 * 1000);
-
- // Calculate the time until midnight of the next day in Japan timezone
- const midnightTomorrowJapan = new Date(
- nowJapan.getFullYear(),
- nowJapan.getMonth(),
- nowJapan.getDate() + 1,
- 0,
- 0,
- 0,
- 0
- );
- const timeUntilMidnightJapan = Math.round(
- (midnightTomorrowJapan - nowJapan) / 1000
- );
-
- let cachedData;
-
- // Check if the data is already in Redis
- if (redis) {
- cachedData = await redis.get("new_schedule");
- }
-
- if (cachedData) {
- const scheduleByDay = JSON.parse(cachedData);
-
- return {
- props: {
- schedule: scheduleByDay,
- // today: todaySchedule,
- },
- };
- } else {
- now.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000
- const dayInSeconds = 86400; // Number of seconds in a day
- const yesterdayStart = Math.floor(now.getTime() / 1000) - dayInSeconds;
- // Calculate weekStart from yesterday's 00:00:00
- const weekStart = yesterdayStart;
- const weekEnd = weekStart + 604800;
-
- let page = 1;
- const airingSchedules = [];
-
- while (true) {
- const res = await fetch("https://graphql.anilist.co", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json",
- },
- body: JSON.stringify({
- query: scheduleQuery,
- variables: {
- weekStart,
- weekEnd,
- page,
- },
- }),
- });
-
- const json = await res.json();
- const schedules = json.data.Page.airingSchedules;
-
- if (schedules.length === 0) {
- break; // No more data to fetch
- }
-
- airingSchedules.push(...schedules);
- page++;
- }
-
- const timestampToDay = (timestamp) => {
- const options = { weekday: "long" };
- return new Date(timestamp * 1000).toLocaleDateString(undefined, options);
- };
-
- const scheduleByDay = {};
- airingSchedules.forEach((schedule) => {
- const day = timestampToDay(schedule.airingAt);
- if (!scheduleByDay[day]) {
- scheduleByDay[day] = [];
- }
- scheduleByDay[day].push(schedule);
- });
-
- if (redis) {
- await redis.set(
- "new_schedule",
- JSON.stringify(scheduleByDay),
- "EX",
- timeUntilMidnightJapan
- );
- }
-
- return {
- props: {
- schedule: scheduleByDay,
- // today: todaySchedule,
- },
- };
- }
- // setSchedule(scheduleByDay);
-}
-
-export default function Schedule({ schedule }) {
- const { data: session } = useSession();
-
- // const [schedule, setSchedule] = useState({});
- const [filterDay, setFilterDay] = useState("All");
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- setLoading(true);
- async function setDay() {
- const now = new Date();
- const today = day[now.getDay()];
- setFilterDay(today);
- setLoading(false);
- }
- setDay();
- }, []);
- // Sort the schedule object by day, placing today's schedule first
- const sortedSchedule = sortScheduleByDay(schedule);
- const formattedSchedule = transformSchedule(schedule);
-
- // State to keep track of the next airing anime
- const [nextAiringAnime, setNextAiringAnime] = useState(null);
- // const [nextAiringBanner, setNextAiringBanner] = useState(null);
-
- // State to keep track of the currently airing anime
- const [currentlyAiringAnime, setCurrentlyAiringAnime] = useState(null);
-
- const [layout, setLayout] = useState(1);
-
- // Effect to update the next and currently airing anime
- useEffect(() => {
- const now = new Date().getTime() / 1000; // Current time in seconds
- let nextAiring = null;
- let currentlyAiring = null;
-
- for (const [, schedules] of Object.entries(sortedSchedule)) {
- for (const s of schedules) {
- if (s.airingAt > now) {
- if (!nextAiring) {
- nextAiring = s.id;
- // setNextAiringBanner(s.media.bannerImage);
- }
- } else if (s.airingAt + 1440 > now) {
- currentlyAiring = s.id;
- }
- }
- if (nextAiring && currentlyAiring) break;
- }
-
- setNextAiringAnime(nextAiring);
- setCurrentlyAiringAnime(currentlyAiring);
- }, [sortedSchedule]);
-
- const scrollContainerRef = useRef(null);
-
- useEffect(() => {
- // Scroll to center the active button when it changes
- if (scrollContainerRef.current) {
- const activeButton =
- scrollContainerRef.current.querySelector(".text-action");
- if (activeButton) {
- const containerWidth = scrollContainerRef.current.clientWidth;
- const buttonLeft = activeButton.offsetLeft;
- const buttonWidth = activeButton.clientWidth;
- const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2;
- scrollContainerRef.current.scrollLeft = scrollLeft;
- }
- }
- }, [filterDay]);
-
- return (
- <>
-
-
Moopa - Schedule
- {/* write a meta with good seo for this page */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
-
*/}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setFilterDay("All")}
- className={`hover:text-action transition-all duration-200 ease-out cursor-pointer ${
- filterDay === "All" ? "text-action" : ""
- }`}
- >
- All
-
- {day.map((i) => (
- {
- setLoading(true);
- setFilterDay(i);
- setLoading(false);
- }}
- className={`py-2 lg:py-0 outline-none hover:text-action transition-all duration-200 ease-out cursor-pointer ${
- filterDay === i ? "text-action" : ""
- }`}
- >
- {i}
-
- ))}
-
-
- setLayout(1)}
- />
- setLayout(2)}
- />
-
-
-
- {layout === 1 ? (
- !loading ? (
- Object.entries(
- filterFormattedSchedule(formattedSchedule, filterDay)
- ).map(([day, timeSlots], index) => (
-
-
- {day}
-
- {Object.entries(timeSlots).map(([time, animeList]) => (
-
-
-
- {timeStamptoAMPM(time)}
-
- {/* {!isAired(time) &&
Airing Next
} */}
-
-
-
- {animeList.map((s, index) => {
- const m = s.media;
- return (
- <>
-
-
-
-
- {m.title.romaji}
-
-
- Ep {s.episode} {timeStamptoHour(s.airingAt)}
-
-
-
-
- >
- );
- })}
-
-
- ))}
-
- ))
- ) : (
-
-
-
- )
- ) : !loading ? (
- Object.entries(filterScheduleByDay(sortedSchedule, filterDay)).map(
- ([day, schedules]) => (
-
-
- {day}
-
-
- {schedules.map((s) => {
- const m = s.media;
-
- return (
-
- {/*
*/}
-
-
- {/* */}
-
- Next Airing
-
-
-
-
-
-
- Airing Now
-
-
-
-
-
- {m.title.romaji}
-
-
- Ep {s.episode} {timeStamptoHour(s.airingAt)}
-
-
-
- );
- })}
-
-
- )
- )
- ) : (
-
-
-
- )}
-
-
- >
- );
-}
diff --git a/pages/en/schedule/index.tsx b/pages/en/schedule/index.tsx
new file mode 100644
index 0000000..aa30259
--- /dev/null
+++ b/pages/en/schedule/index.tsx
@@ -0,0 +1,484 @@
+import Image from "next/image";
+import { useEffect, useRef, useState } from "react";
+import Link from "next/link";
+import { CalendarIcon } from "@heroicons/react/24/solid";
+import { ClockIcon } from "@heroicons/react/24/outline";
+import Loading from "@/components/shared/loading";
+import { timeStamptoAMPM, timeStamptoHour } from "@/utils/getTimes";
+import {
+ filterFormattedSchedule,
+ filterScheduleByDay,
+ sortScheduleByDay,
+ transformSchedule,
+} from "@/utils/schedulesUtils";
+
+import { scheduleQuery } from "@/lib/graphql/query";
+import MobileNav from "@/components/shared/MobileNav";
+
+import { useSession } from "next-auth/react";
+import { redis } from "@/lib/redis";
+import Head from "next/head";
+import { Navbar } from "@/components/shared/NavBar";
+
+const day = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+];
+
+const isAired = (timestamp: number | null) => {
+ if (!timestamp) return false;
+ const currentTime = new Date().getTime() / 1000;
+ return timestamp <= currentTime;
+};
+
+export async function getServerSideProps() {
+ const now = new Date();
+ // Adjust for Japan timezone (add 9 hours)
+ const nowJapan = new Date(now.getTime() + 9 * 60 * 60 * 1000);
+
+ // Calculate the time until midnight of the next day in Japan timezone
+ const midnightTomorrowJapan = new Date(
+ nowJapan.getFullYear(),
+ nowJapan.getMonth(),
+ nowJapan.getDate() + 1,
+ 0,
+ 0,
+ 0,
+ 0
+ );
+ const timeUntilMidnightJapan = Math.round(
+ (midnightTomorrowJapan.getTime() - nowJapan.getTime()) / 1000
+ );
+
+ let cachedData;
+
+ // Check if the data is already in Redis
+ if (redis) {
+ cachedData = await redis.get("new_schedule");
+ }
+
+ if (cachedData) {
+ const scheduleByDay = JSON.parse(cachedData);
+
+ return {
+ props: {
+ schedule: scheduleByDay,
+ // today: todaySchedule,
+ },
+ };
+ } else {
+ now.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000
+ const dayInSeconds = 86400; // Number of seconds in a day
+ const yesterdayStart = Math.floor(now.getTime() / 1000) - dayInSeconds;
+ // Calculate weekStart from yesterday's 00:00:00
+ const weekStart = yesterdayStart;
+ const weekEnd = weekStart + 604800;
+
+ let page = 1;
+ const airingSchedules = [];
+
+ while (true) {
+ const res = await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ query: scheduleQuery,
+ variables: {
+ weekStart,
+ weekEnd,
+ page,
+ },
+ }),
+ });
+
+ const json = await res.json();
+ const schedules = json.data.Page.airingSchedules;
+
+ if (schedules.length === 0) {
+ break; // No more data to fetch
+ }
+
+ airingSchedules.push(...schedules);
+ page++;
+ }
+
+ const timestampToDay = (timestamp: number) => {
+ return new Date(timestamp * 1000).toLocaleDateString(undefined, {
+ weekday: "long",
+ });
+ };
+
+ const scheduleByDay: { [key: string]: any } = {};
+ airingSchedules.forEach((schedule) => {
+ const day = timestampToDay(schedule.airingAt);
+ if (!scheduleByDay[day]) {
+ scheduleByDay[day] = [];
+ }
+ scheduleByDay[day].push(schedule);
+ });
+
+ if (redis) {
+ await redis.set(
+ "new_schedule",
+ JSON.stringify(scheduleByDay),
+ "EX",
+ timeUntilMidnightJapan
+ );
+ }
+
+ return {
+ props: {
+ schedule: scheduleByDay,
+ // today: todaySchedule,
+ },
+ };
+ }
+ // setSchedule(scheduleByDay);
+}
+
+export default function Schedule({ schedule }: any) {
+ const [filterDay, setFilterDay] = useState("All");
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ async function setDay() {
+ const now = new Date();
+ const today = day[now.getDay()];
+ setFilterDay(today);
+ setLoading(false);
+ }
+ setDay();
+ }, []);
+ // Sort the schedule object by day, placing today's schedule first
+ const sortedSchedule = sortScheduleByDay(schedule);
+ const formattedSchedule = transformSchedule(schedule);
+
+ // State to keep track of the next airing anime
+ const [nextAiringAnime, setNextAiringAnime] = useState(null);
+ // const [nextAiringBanner, setNextAiringBanner] = useState(null);
+
+ // State to keep track of the currently airing anime
+ const [currentlyAiringAnime, setCurrentlyAiringAnime] = useState(null);
+
+ const [layout, setLayout] = useState(1);
+
+ // Effect to update the next and currently airing anime
+ useEffect(() => {
+ const now = new Date().getTime() / 1000; // Current time in seconds
+ let nextAiring = null;
+ let currentlyAiring = null;
+
+ for (const [, schedules] of Object.entries(sortedSchedule as object)) {
+ for (const s of schedules) {
+ if (s.airingAt > now) {
+ if (!nextAiring) {
+ nextAiring = s.id;
+ // setNextAiringBanner(s.media.bannerImage);
+ }
+ } else if (s.airingAt + 1440 > now) {
+ currentlyAiring = s.id;
+ }
+ }
+ if (nextAiring && currentlyAiring) break;
+ }
+
+ setNextAiringAnime(nextAiring);
+ setCurrentlyAiringAnime(currentlyAiring);
+ }, [sortedSchedule]);
+
+ const scrollContainerRef = useRef
(null);
+
+ useEffect(() => {
+ // Scroll to center the active button when it changes
+ if (scrollContainerRef.current) {
+ const activeButton =
+ scrollContainerRef.current?.querySelector(".text-action");
+ if (activeButton) {
+ const containerWidth = scrollContainerRef.current.clientWidth;
+ const buttonLeft = (activeButton as HTMLElement).offsetLeft;
+ const buttonWidth = activeButton.clientWidth;
+ const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2;
+ scrollContainerRef.current.scrollLeft = scrollLeft;
+ }
+ }
+ }, [filterDay]);
+
+ return (
+ <>
+
+ Moopa - Schedule
+ {/* write a meta with good seo for this page */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilterDay("All")}
+ className={`hover:text-action transition-all duration-200 ease-out cursor-pointer ${
+ filterDay === "All" ? "text-action" : ""
+ }`}
+ >
+ All
+
+ {day.map((i) => (
+ {
+ setLoading(true);
+ setFilterDay(i);
+ setLoading(false);
+ }}
+ className={`py-2 lg:py-0 outline-none hover:text-action transition-all duration-200 ease-out cursor-pointer ${
+ filterDay === i ? "text-action" : ""
+ }`}
+ >
+ {i}
+
+ ))}
+
+
+ setLayout(1)}
+ />
+ setLayout(2)}
+ />
+
+
+
+ {layout === 1 ? (
+ !loading ? (
+ Object.entries(
+ filterFormattedSchedule(formattedSchedule, filterDay)
+ ).map(([day, timeSlots], index) => (
+
+
+ {day}
+
+ {Object.entries(timeSlots).map(([time, animeList]) => (
+
+
+
+ {time && timeStamptoAMPM(time)}
+
+ {/* {!isAired(time) &&
Airing Next
} */}
+
+
+
+ {animeList.map((s, index) => {
+ const m = s.media;
+ return (
+ <>
+
+
+
+
+ {m.title.romaji}
+
+
+ Ep {s.episode} {timeStamptoHour(s.airingAt)}
+
+
+
+
+ >
+ );
+ })}
+
+
+ ))}
+
+ ))
+ ) : (
+
+
+
+ )
+ ) : !loading ? (
+ Object.entries(filterScheduleByDay(sortedSchedule, filterDay)).map(
+ ([day, schedules]) => (
+
+
+ {day}
+
+
+ {schedules.map((s) => {
+ const m = s.media;
+
+ return (
+
+ {/*
*/}
+
+
+ {/* */}
+
+ Next Airing
+
+
+
+
+
+
+ Airing Now
+
+
+
+
+
+ {m.title.romaji}
+
+
+ Ep {s.episode} {timeStamptoHour(s.airingAt)}
+
+
+
+ );
+ })}
+
+
+ )
+ )
+ ) : (
+
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js
deleted file mode 100644
index c1fd94c..0000000
--- a/pages/en/search/[...param].js
+++ /dev/null
@@ -1,571 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { motion as m } from "framer-motion";
-import Skeleton from "react-loading-skeleton";
-import { useRouter } from "next/router";
-import Link from "next/link";
-import Head from "next/head";
-import Footer from "@/components/shared/footer";
-
-import Image from "next/image";
-import { aniAdvanceSearch } from "@/lib/anilist/aniAdvanceSearch";
-import MultiSelector from "@/components/search/dropdown/multiSelector";
-import SingleSelector from "@/components/search/dropdown/singleSelector";
-import {
- animeFormatOptions,
- formatOptions,
- genreOptions,
- mangaFormatOptions,
- mediaType,
- seasonOptions,
- tagsOption,
- yearOptions,
-} from "@/components/search/selection";
-import InputSelect from "@/components/search/dropdown/inputSelect";
-import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
-import useDebounce from "@/lib/hooks/useDebounce";
-import { NewNavbar } from "@/components/shared/NavBar";
-import MobileNav from "@/components/shared/MobileNav";
-import SearchByImage from "@/components/search/searchByImage";
-import { PlayIcon } from "@heroicons/react/24/outline";
-
-export async function getServerSideProps(context) {
- const { param } = context.query;
-
- const { search, format, genres, season, year } = context.query;
-
- let getFormat, getSeason, getYear;
- let getGenres = [];
-
- if (genres) {
- const gr = genreOptions.find(
- (i) => i.value.toLowerCase() === genres.toLowerCase()
- );
- getGenres.push(gr);
- }
-
- if (season) {
- getSeason = seasonOptions.find(
- (i) => i.value.toLowerCase() === season.toLowerCase()
- );
- if (!year) {
- const now = new Date().getFullYear();
- getYear = yearOptions.find((i) => i.value === now.toString());
- } else {
- getYear = yearOptions.find((i) => i.value === year);
- }
- }
-
- if (format) {
- getFormat = formatOptions.find(
- (i) => i.value.toLowerCase() === format.toLowerCase()
- );
- }
-
- if (!param && param.length !== 1) {
- return {
- notFound: true,
- };
- }
-
- const typeIndex = param[0] === "anime" ? 0 : 1;
-
- return {
- props: {
- index: typeIndex,
- query: search || null,
- formats: getFormat || null,
- seasons: getSeason || null,
- years: getYear || null,
- genres: getGenres || null,
- },
- };
-}
-
-export default function Card({
- index,
- query,
- genres,
- formats,
- seasons,
- years,
-}) {
- const inputRef = useRef(null);
- const router = useRouter();
-
- const [data, setData] = useState();
- const [imageSearch, setImageSearch] = useState();
-
- const [loading, setLoading] = useState(true);
-
- const [search, setQuery] = useState(query);
- const debounceSearch = useDebounce(search, 500);
-
- const [type, setSelectedType] = useState(mediaType[index]);
- const [year, setYear] = useState(years);
- const [season, setSeason] = useState(seasons);
- const [sort, setSelectedSort] = useState();
- const [genre, setGenre] = useState(genres);
- const [format, setFormat] = useState(formats);
-
- const [isVisible, setIsVisible] = useState(false);
-
- const [page, setPage] = useState(1);
- const [nextPage, setNextPage] = useState(true);
-
- async function advance() {
- setLoading(true);
- const data = await aniAdvanceSearch({
- search: debounceSearch,
- type: type?.value,
- genres: genre,
- page: page,
- sort: sort?.value,
- format: format?.value,
- season: season?.value,
- seasonYear: year?.value,
- });
- if (data?.media?.length === 0) {
- setNextPage(false);
- setLoading(false);
- } else if (data !== null && page > 1) {
- setData((prevData) => {
- return [...(prevData ?? []), ...data?.media];
- });
- setNextPage(data?.pageInfo.hasNextPage);
- setLoading(false);
- } else {
- setData(data?.media);
- setNextPage(data?.pageInfo.hasNextPage);
- setLoading(false);
- }
- }
-
- useEffect(() => {
- setData(null);
- setPage(1);
- setNextPage(true);
- advance();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- debounceSearch,
- type?.value,
- sort?.value,
- genre,
- format?.value,
- season?.value,
- year?.value,
- ]);
-
- useEffect(() => {
- if (imageSearch) return;
- advance();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [page, imageSearch]);
-
- useEffect(() => {
- function handleScroll() {
- if (imageSearch) {
- window.removeEventListener("scroll", handleScroll);
- return;
- }
- if (page > 10 || !nextPage) {
- window.removeEventListener("scroll", handleScroll);
- return;
- }
-
- if (
- window.innerHeight + window.pageYOffset >=
- document.body.offsetHeight - 3
- ) {
- setPage((prevPage) => prevPage + 1);
- }
- }
-
- window.addEventListener("scroll", handleScroll);
-
- return () => window.removeEventListener("scroll", handleScroll);
- }, [page, nextPage, imageSearch]);
-
- const handleKeyDown = async (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- const inputValue = event.target.value;
- if (inputValue === "") {
- setQuery(null);
- } else {
- setQuery(inputValue);
- }
- }
- };
-
- function trash() {
- setImageSearch();
- setQuery();
- setGenre();
- setFormat();
- setSelectedSort();
- setSeason();
- setYear();
- router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`);
- }
-
- function handleVisible() {
- setIsVisible(!isVisible);
- }
-
- const handleVideoHover = (hovered, id) => {
- const updatedImageSearch = imageSearch?.map((item) => {
- if (item.filename === id) {
- return { ...item, hovered };
- }
- return item;
- });
- setImageSearch(updatedImageSearch);
- };
-
- // console.log({ loading, data });
-
- return (
- <>
-
- Moopa - search
-
-
-
-
-
-
-
-
-
-
-
-
- {/* GENRES */}
-
- {/* SORT */}
- {/* */}
- {/* FORMAT */}
-
- {/* SEASON */}
-
- {/* YEAR */}
-
-
-
-
-
-
-
-
- {isVisible && (
-
-
- {/* GENRES */}
-
- {/* SORT */}
- {/* */}
- {/* FORMAT */}
-
- {/* SEASON */}
-
- {/* YEAR */}
-
-
-
- )}
- {/*
*/}
-
-
- {loading
- ? ""
- : !data && (
-
- Oops! Nothing's Found...
-
- )}
-
- {data &&
- data?.length > 0 &&
- !imageSearch &&
- data?.map((anime, index) => {
- const anilistId = anime?.mappings?.find(
- (x) => x.providerId === "anilist"
- )?.id;
- return (
-
-
-
-
-
-
- {anime.status === "RELEASING" ? (
-
- ) : anime.status === "NOT_YET_RELEASED" ? (
-
- ) : null}
- {anime.title.userPreferred}
-
-
-
- {anime.format || -
} ·{" "}
- {anime.status || -
} ·{" "}
- {anime.episodes
- ? `${anime.episodes || "N/A"} Episodes`
- : `${anime.chapters || "N/A"} Chapters`}
-
-
- );
- })}
-
- {loading && (
- <>
- {[1, 2, 4, 5, 6, 7, 8].map((item) => (
-
- ))}
- >
- )}
-
-
- {imageSearch && (
-
- {imageSearch.map((a, index) => {
- return (
-
- {
- handleVideoHover(true, a.filename);
- }}
- onMouseLeave={() => handleVideoHover(false, a.filename)}
- >
-
-
-
-
- {`Episode ${a.episode}`}
-
-
-
- {a?.image && (
-
- )}
- {a?.video && (
-
- )}
-
-
-
- {/* {a.title} */}
-
-
- {a?.anilist.title.romaji}
- {" "}
- | Episode {a.episode}
-
-
-
- );
- })}
-
- )}
- {!loading && page > 10 && nextPage && (
-
setPage((p) => p + 1)}
- className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
- >
- Load More
-
- )}
-
- {/*
*/}
-
-
-
- >
- );
-}
diff --git a/pages/en/search/[...param].tsx b/pages/en/search/[...param].tsx
new file mode 100644
index 0000000..5a34ff5
--- /dev/null
+++ b/pages/en/search/[...param].tsx
@@ -0,0 +1,598 @@
+import { Key, useEffect, useRef, useState } from "react";
+import { motion as m } from "framer-motion";
+import Skeleton from "react-loading-skeleton";
+import { useRouter } from "next/router";
+import Link from "next/link";
+import Head from "next/head";
+import Footer from "@/components/shared/footer";
+
+import Image from "next/image";
+import { aniAdvanceSearch } from "@/lib/anilist/aniAdvanceSearch";
+import MultiSelector from "@/components/search/dropdown/multiSelector";
+import SingleSelector from "@/components/search/dropdown/singleSelector";
+import {
+ animeFormatOptions,
+ formatOptions,
+ genreOptions,
+ mangaFormatOptions,
+ mediaType,
+ seasonOptions,
+ tagsOption,
+ yearOptions,
+} from "@/components/search/selection";
+import InputSelect from "@/components/search/dropdown/inputSelect";
+import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
+import useDebounce from "@/lib/hooks/useDebounce";
+import { Navbar } from "@/components/shared/NavBar";
+import MobileNav from "@/components/shared/MobileNav";
+import SearchByImage, {
+ TraceMoeResultTypes,
+} from "@/components/search/searchByImage";
+import { PlayIcon } from "@heroicons/react/24/outline";
+import { StaticImport } from "next/dist/shared/lib/get-img-props";
+
+export async function getServerSideProps(context: any) {
+ const { param } = context.query;
+
+ const { search, format, genres, season, year } = context.query;
+
+ let getFormat, getSeason, getYear;
+ let getGenres = [];
+
+ if (genres) {
+ const gr = genreOptions.find(
+ (i) => i.value.toLowerCase() === genres.toLowerCase()
+ );
+ getGenres.push(gr);
+ }
+
+ if (season) {
+ getSeason = seasonOptions.find(
+ (i) => i.value.toLowerCase() === season.toLowerCase()
+ );
+ if (!year) {
+ const now = new Date().getFullYear();
+ getYear = yearOptions.find((i) => i.value === now.toString());
+ } else {
+ getYear = yearOptions.find((i) => i.value === year);
+ }
+ }
+
+ if (format) {
+ getFormat = formatOptions.find(
+ (i) => i.value.toLowerCase() === format.toLowerCase()
+ );
+ }
+
+ if (!param && param.length !== 1) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const typeIndex = param[0] === "anime" ? 0 : 1;
+
+ return {
+ props: {
+ index: typeIndex,
+ query: search || null,
+ formats: getFormat || null,
+ seasons: getSeason || null,
+ years: getYear || null,
+ genres: getGenres || null,
+ },
+ };
+}
+
+type CardProps = {
+ index: number;
+ query: string;
+ genres: any;
+ formats: any;
+ seasons: any;
+ years: any;
+};
+
+export default function Card({
+ index,
+ query,
+ genres,
+ formats,
+ seasons,
+ years,
+}: CardProps) {
+ const inputRef = useRef(null);
+ const router = useRouter();
+
+ const [data, setData] = useState();
+ const [imageSearch, setImageSearch] = useState();
+
+ const [loading, setLoading] = useState(true);
+
+ const [search, setQuery] = useState(query);
+ const debounceSearch = useDebounce(search, 500);
+
+ const [type, setSelectedType] = useState<{
+ name: string;
+ value: string;
+ } | null>(mediaType[index]);
+ const [year, setYear] = useState(years);
+ const [season, setSeason] = useState(seasons);
+ const [sort, setSelectedSort] = useState<{ name: string; value: string }>();
+ const [genre, setGenre] = useState(genres);
+ const [format, setFormat] = useState(formats);
+
+ const [isVisible, setIsVisible] = useState(false);
+
+ const [page, setPage] = useState(1);
+ const [nextPage, setNextPage] = useState(true);
+
+ async function advance() {
+ setLoading(true);
+ const data = await aniAdvanceSearch({
+ search: debounceSearch,
+ type: type?.value as "ANIME" | "MANGA" | undefined,
+ genres: genre,
+ page: page,
+ sort: sort?.value,
+ format: format?.value,
+ season: season?.value,
+ seasonYear: year?.value,
+ });
+ if (data?.media?.length === 0) {
+ setNextPage(false);
+ setLoading(false);
+ } else if (data !== null && page > 1) {
+ setData((prevData: any) => {
+ return [...(prevData ?? []), ...data?.media];
+ });
+ setNextPage(data?.pageInfo.hasNextPage);
+ setLoading(false);
+ } else {
+ setData(data?.media);
+ setNextPage(data?.pageInfo.hasNextPage);
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ setData(null);
+ setPage(1);
+ setNextPage(true);
+ if (page === 1) {
+ advance();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ debounceSearch,
+ type?.value,
+ sort?.value,
+ genre,
+ format?.value,
+ season?.value,
+ year?.value,
+ ]);
+
+ useEffect(() => {
+ if (imageSearch) return;
+ if (page > 1) {
+ advance();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [page, imageSearch]);
+
+ useEffect(() => {
+ function handleScroll() {
+ if (imageSearch) {
+ window.removeEventListener("scroll", handleScroll);
+ return;
+ }
+ if (page > 10 || !nextPage) {
+ window.removeEventListener("scroll", handleScroll);
+ return;
+ }
+
+ if (
+ window.innerHeight + window.pageYOffset >=
+ document.body.offsetHeight - 3
+ ) {
+ if (!loading) {
+ setPage((prevPage) => prevPage + 1);
+ }
+ }
+ }
+
+ window.addEventListener("scroll", handleScroll);
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [page, nextPage, imageSearch, loading]);
+
+ const handleKeyDown = async (event: any) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ const inputValue = event.target.value;
+ if (inputValue === "") {
+ setQuery(undefined);
+ } else {
+ setQuery(inputValue);
+ }
+ }
+ };
+
+ function trash() {
+ setImageSearch(undefined);
+ setQuery(undefined);
+ setGenre(undefined);
+ setFormat(undefined);
+ setSelectedSort(undefined);
+ setSeason(undefined);
+ setYear(undefined);
+ router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`);
+ }
+
+ function handleVisible() {
+ setIsVisible(!isVisible);
+ }
+
+ const handleVideoHover = (hovered: boolean, id: any) => {
+ const updatedImageSearch = imageSearch?.map((item: any) => {
+ if (item.filename === id) {
+ return { ...item, hovered };
+ }
+ return item;
+ });
+ setImageSearch(updatedImageSearch);
+ };
+
+ // console.log({ loading, data });
+
+ return (
+ <>
+
+ Moopa - search
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* GENRES */}
+
+ {/* SORT */}
+ {/* */}
+ {/* FORMAT */}
+
+ {/* SEASON */}
+
+ {/* YEAR */}
+
+
+
+
+
+
+
+
+ {isVisible && (
+
+
+ {/* GENRES */}
+
+ {/* SORT */}
+ {/* */}
+ {/* FORMAT */}
+
+ {/* SEASON */}
+
+ {/* YEAR */}
+
+
+
+ )}
+ {/*
*/}
+
+
+ {loading
+ ? ""
+ : !data && (
+
+ Oops! Nothing's Found...
+
+ )}
+
+ {data &&
+ data?.length > 0 &&
+ !imageSearch &&
+ data?.map(
+ (
+ anime: {
+ format: string;
+ id: any;
+ title: { userPreferred: string };
+ coverImage: { extraLarge: string | StaticImport };
+ status: string;
+ episodes: any;
+ chapters: any;
+ },
+ index: Key | null | undefined
+ ) => {
+ return (
+
+
+
+
+
+
+ {anime.status === "RELEASING" ? (
+
+ ) : anime.status === "NOT_YET_RELEASED" ? (
+
+ ) : null}
+ {anime.title.userPreferred}
+
+
+
+ {anime.format || -
} ·{" "}
+ {anime.status || -
} ·{" "}
+ {anime.episodes
+ ? `${anime.episodes || "N/A"} Episodes`
+ : `${anime.chapters || "N/A"} Chapters`}
+
+
+ );
+ }
+ )}
+
+ {loading && (
+ <>
+ {[1, 2, 4, 5, 6, 7, 8].map((item) => (
+
+ ))}
+ >
+ )}
+
+
+ {imageSearch && (
+
+ {imageSearch.map((a, index) => {
+ return (
+
+ {
+ handleVideoHover(true, a.filename);
+ }}
+ onMouseLeave={() => handleVideoHover(false, a.filename)}
+ >
+
+
+
+
+ {`Episode ${a.episode}`}
+
+
+
+ {a?.image && (
+
+ )}
+ {a?.video && (
+
+ )}
+
+
+
+ {/* {a.title} */}
+
+
+ {a?.anilist.title.romaji}
+ {" "}
+ | Episode {a.episode}
+
+
+
+ );
+ })}
+
+ )}
+ {!loading && page > 10 && nextPage && (
+
setPage((p) => p + 1)}
+ className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
+ >
+ Load More
+
+ )}
+
+ {/*
*/}
+
+
+
+ >
+ );
+}
diff --git a/pages/id/index.js b/pages/id/index.js
deleted file mode 100644
index 5ef870d..0000000
--- a/pages/id/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Head from "next/head";
-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 MobileNav from "@/components/shared/MobileNav";
-
-export default function Home() {
- return (
- <>
-
- Under Construction
-
-
-
-
-
-
-
- {/* Create an under construction page with tailwind css */}
-
-
-
- 🚧 Page Under Construction 🚧
-
-
- "Please be patient, as we're still working on this page and it will
- be available soon."
-
-
-
- Go back home
-
-
-
-
-
- >
- );
-}
diff --git a/pages/id/index.tsx b/pages/id/index.tsx
new file mode 100644
index 0000000..9af2d06
--- /dev/null
+++ b/pages/id/index.tsx
@@ -0,0 +1,47 @@
+import Head from "next/head";
+import React from "react";
+import Image from "next/image";
+import Link from "next/link";
+import Footer from "@/components/shared/footer";
+import { Navbar } from "@/components/shared/NavBar";
+import MobileNav from "@/components/shared/MobileNav";
+
+export default function Home() {
+ return (
+ <>
+
+ Under Construction
+
+
+
+
+
+
+
+ {/* Create an under construction page with tailwind css */}
+
+
+
+ 🚧 Page Under Construction 🚧
+
+
+ "Please be patient, as we're still working on this page and it will
+ be available soon."
+
+
+
+ Go back home
+
+
+
+
+
+ >
+ );
+}
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(null);
+ const [loading, setLoading] = useState(true);
+
+ const [filter, setFilter] = useState("");
+
+ 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 (
+
+
+
+
+ {data && (
+
+
+ {data?.image && (
+
+ )}
+
+
+
+ {data?.title}
+
+ {/*
+
+ Format: {data?.format}
+
+
+ Release: {data?.year}
+
+
+ Status: {data?.status}
+
+
*/}
+
+ {data?.description}
+
+
+
+ )}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+
+ {filteredData?.map((chapter: ChapterType) => (
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+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();
+ 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 (
+
+ {!hideNav && (
+ <>
+
+
+ >
+ )}
+
setHideNav((prev) => !prev)}>
+
+ {data?.pages?.map((i) => (
+
+
+
+ ))}
+
+
+
+ );
+}
+
+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();
+ 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 (
+
+
+
+
+ {data && (
+
+ {data?.image && (
+
+ )}
+
+
+ {data?.title}
+
+
+
+ Release: {data?.Release}
+
+
+ Status: {data?.Status}
+
+
+ Author: {data?.Author}
+
+
+
+ {data?.description}
+
+
+
+ )}
+
+
+ {data?.chapters?.map((chapter) => (
+
+
+
{chapter?.chapter}
+
{chapter?.release}
+
+
+ ))}
+
+
+
+
+ );
+}
+
+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();
+
+ 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 (
+ <>
+
+
+
+ {/* {data && ( */}
+
+
+
+ prev
+
+
+ next
+
+
+
/
+
+ {data?.novelTitle}
+
+
+ {/* )} */}
+
+
+
{data?.title}
+ {data?.content && (
+
+ )}
+
+
+ {data?.content && (
+
+
+
+ prev
+
+
+ next
+
+
+
+ )}
+
+ >
+ );
+}
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([]);
+ 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 (
+
+
+
+
+
handleChange(e)}>
+
+ {type.name}
+
+
+ setQuery("")}
+ >
+
+ {types.length === 0 && query !== "" ? (
+
+ Nothing found.
+
+ ) : (
+ types.map((item) => (
+
+ `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 }) => (
+
+
+ {item.name}
+
+ {selected ? (
+
+
+
+ ) : null}
+
+ )}
+
+ ))
+ )}
+
+
+
+
+
+
+
+ {data !== null
+ ? data?.map((x, index) => (
+
+
+ {x.img && (
+
+ )}
+
+
+
+ {x.title}
+
+
+
+ ))
+ : "No results found"}
+
+
+
+ );
+}
diff --git a/pages/index.js b/pages/index.js
deleted file mode 100644
index 25d5b20..0000000
--- a/pages/index.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Head from "next/head";
-
-export default function Home() {
- return (
- <>
-
-
-
-
-
-
- >
- );
-}
-
-export async function getServerSideProps() {
- return {
- redirect: {
- destination: "/en",
- permanent: false,
- },
- };
-}
diff --git a/pages/index.tsx b/pages/index.tsx
new file mode 100644
index 0000000..25d5b20
--- /dev/null
+++ b/pages/index.tsx
@@ -0,0 +1,32 @@
+import Head from "next/head";
+
+export default function Home() {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export async function getServerSideProps() {
+ return {
+ redirect: {
+ destination: "/en",
+ permanent: false,
+ },
+ };
+}
diff --git a/prisma/user.js b/prisma/user.js
deleted file mode 100644
index c2ba5fd..0000000
--- a/prisma/user.js
+++ /dev/null
@@ -1,298 +0,0 @@
-import { Prisma } from "@prisma/client";
-// const prisma = new PrismaClient();
-
-import { prisma } from "../lib/prisma";
-
-export const createUser = async (name) => {
- try {
- const checkUser = await prisma.userProfile.findUnique({
- where: {
- name: name,
- },
- });
- if (!checkUser) {
- const user = await prisma.userProfile.create({
- data: {
- name: name,
- },
- });
-
- return user;
- } else {
- return null;
- }
- } catch (error) {
- if (error instanceof Prisma.PrismaClientKnownRequestError) {
- if (error.code === "P2002") {
- console.log(
- "There is a unique constraint violation, a new user cannot be created with this name"
- );
- }
- } else if (error instanceof Prisma.PrismaClientUnknownRequestError) {
- console.log("An unknown Prisma error occurred:", error.message);
- }
- console.error(error);
- throw new Error("Error creating user");
- }
-};
-
-export const updateUser = async (name, setting) => {
- try {
- const user = await prisma.userProfile.updateMany({
- where: {
- name: name,
- },
- data: {
- setting,
- },
- });
- return user;
- // const checkAnime = await prisma.watchListItem.findUnique({
- // where: {
- // title: anime.title,
- // userProfileId: name,
- // },
- // });
- // if (checkAnime) {
- // const checkEpisode = await prisma.watchListEpisode.findUnique({
- // where: {
- // url: anime.id,
- // },
- // });
- // if (checkEpisode) {
- // return null;
- // } else {
- // const user = await prisma.watchListItem.update({
- // where: {
- // title: anime.title,
- // userProfileId: name,
- // },
- // });
- // }
- // } else {
- // const user = await prisma.userProfile.update({
- // where: { name: name },
- // data: {
- // watchList: {
- // create: {
- // title: anime.title,
- // episodes: {
- // create: {
- // url: anime.id,
- // },
- // },
- // },
- // },
- // },
- // include: {
- // watchList: true,
- // },
- // });
-
- // return user;
- // }
- } catch (error) {
- console.error(error);
- throw new Error("Error updating user");
- }
-};
-
-export const getUser = async (name, list = true) => {
- try {
- if (!name) {
- const user = await prisma.userProfile.findMany({
- include: {
- WatchListEpisode: list,
- },
- });
- return user;
- } else {
- const user = await prisma.userProfile.findFirst({
- where: {
- name: name,
- },
- include: {
- WatchListEpisode: {
- orderBy: {
- createdDate: "desc",
- },
- },
- },
- });
- return user;
- }
- } catch (error) {
- console.error(error);
- throw new Error("Error getting user");
- }
-};
-
-export const deleteUser = async (name) => {
- try {
- const user = await prisma.userProfile.delete({
- where: {
- name: name,
- },
- });
- return user;
- } catch (error) {
- console.error(error);
- throw new Error("Error deleting user");
- }
-};
-
-export const createList = async (name, id, title) => {
- try {
- const checkEpisode = await prisma.watchListEpisode.findFirst({
- where: {
- userProfileId: name,
- watchId: id,
- },
- });
- if (checkEpisode) {
- return null;
- } else {
- const episode = await prisma.userProfile.update({
- where: { name: name },
- data: {
- WatchListEpisode: {
- create: [
- {
- watchId: id,
- },
- ],
- },
- },
- include: {
- WatchListEpisode: true,
- },
- });
- return episode;
- }
- } catch (error) {
- console.error(error);
- throw new Error("Error creating list");
- }
-};
-
-export const getEpisode = async (name, id) => {
- try {
- const episode = await prisma.watchListEpisode.findMany({
- where: {
- AND: [
- {
- userProfileId: name,
- },
- {
- watchId: {
- equals: id,
- },
- },
- ],
- },
- });
- return episode;
- } catch (error) {
- console.error(error);
- throw new Error("Error getting episode");
- }
-};
-
-export const updateUserEpisode = async ({
- name,
- id,
- watchId,
- title,
- image,
- number,
- duration,
- timeWatched,
- aniTitle,
- provider,
- nextId,
- nextNumber,
- dub,
-}) => {
- try {
- const user = await prisma.watchListEpisode.updateMany({
- where: {
- userProfileId: name,
- watchId: watchId,
- },
- data: {
- title: title,
- aniTitle: aniTitle,
- image: image,
- aniId: id,
- provider: provider,
- duration: duration,
- episode: number,
- timeWatched: timeWatched,
- nextId: nextId,
- nextNumber: nextNumber,
- dub: dub,
- createdDate: new Date(),
- },
- });
-
- return user;
- } catch (error) {
- console.error(error);
- throw new Error("Error updating user episode");
- }
-};
-
-export const deleteEpisode = async (name, id) => {
- try {
- const user = await prisma.watchListEpisode.deleteMany({
- where: {
- watchId: id,
- userProfileId: name,
- },
- });
- if (user) {
- return user;
- } else {
- return { message: "Episode not found" };
- }
- } catch (error) {
- console.error(error);
- throw new Error("Error deleting episode");
- }
-};
-
-export const deleteList = async (name, id) => {
- try {
- const user = await prisma.watchListEpisode.deleteMany({
- where: {
- aniId: id,
- userProfileId: name,
- },
- });
- if (user) {
- return user;
- } else {
- return { message: "Episode not found" };
- }
- } catch (error) {
- console.error(error);
- throw new Error("Error deleting list");
- }
-};
-
-// export const updateTimeWatched = async (id, timeWatched) => {
-// try {
-// const user = await prisma.watchListEpisode.update({
-// where: {
-// id: id,
-// },
-// data: {
-// timeWatched: timeWatched,
-// },
-// });
-// return user;
-// } catch (error) {
-// console.error(error);
-// throw new Error("Error updating time watched");
-// }
-// };
diff --git a/prisma/user.ts b/prisma/user.ts
new file mode 100644
index 0000000..8a0d856
--- /dev/null
+++ b/prisma/user.ts
@@ -0,0 +1,288 @@
+import { Prisma, UserProfile, WatchListEpisode } from "@prisma/client";
+
+import { prisma } from "../lib/prisma";
+
+interface UpdateUserEpisodeParams {
+ name: string;
+ id: string;
+ watchId: string;
+ title: string;
+ image: string;
+ number: number;
+ duration: number;
+ timeWatched: number;
+ aniTitle: string;
+ provider: string;
+ nextId: string;
+ nextNumber: number;
+ dub: boolean;
+}
+
+export const createUser = async (name: string): Promise => {
+ try {
+ const checkUser = await prisma.userProfile.findUnique({
+ where: {
+ name: name,
+ },
+ });
+ if (!checkUser) {
+ const user = await prisma.userProfile.create({
+ data: {
+ name: name,
+ },
+ });
+
+ return user;
+ } else {
+ return null;
+ }
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ if (error.code === "P2002") {
+ console.log(
+ "There is a unique constraint violation, a new user cannot be created with this name"
+ );
+ }
+ } else if (error instanceof Prisma.PrismaClientUnknownRequestError) {
+ console.log("An unknown Prisma error occurred:", error.message);
+ }
+ console.error(error);
+ throw new Error("Error creating user");
+ }
+};
+
+export const updateUser = async (
+ name: string,
+ setting: any
+): Promise<{ name: string; setting: any } | null> => {
+ try {
+ await prisma.userProfile.updateMany({
+ where: {
+ name: name,
+ },
+ data: {
+ setting,
+ },
+ });
+ return { name: name, setting: setting };
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error updating user");
+ }
+};
+
+export const getUser = async (
+ name: string,
+ list = true
+): Promise => {
+ try {
+ if (!name) {
+ const user = await prisma.userProfile.findMany({
+ include: {
+ WatchListEpisode: list,
+ },
+ });
+ return user;
+ } else {
+ const user = await prisma.userProfile.findFirst({
+ where: {
+ name: name,
+ },
+ include: {
+ WatchListEpisode: {
+ orderBy: {
+ createdDate: "desc",
+ },
+ },
+ },
+ });
+ return user;
+ }
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error getting user");
+ }
+};
+
+export const deleteUser = async (name: string): Promise => {
+ try {
+ const user = await prisma.userProfile.delete({
+ where: {
+ name: name,
+ },
+ });
+ return user;
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error deleting user");
+ }
+};
+
+export const createList = async (
+ name: string,
+ id: string,
+ title: string
+): Promise => {
+ try {
+ const checkEpisode = await prisma.watchListEpisode.findFirst({
+ where: {
+ userProfileId: name,
+ watchId: id,
+ },
+ });
+ if (checkEpisode) {
+ return null;
+ } else {
+ const episode = await prisma.userProfile.update({
+ where: { name: name },
+ data: {
+ WatchListEpisode: {
+ create: [
+ {
+ watchId: id,
+ },
+ ],
+ },
+ },
+ include: {
+ WatchListEpisode: true,
+ },
+ });
+ return episode;
+ }
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error creating list");
+ }
+};
+
+export const getEpisode = async (
+ name: string,
+ id: string
+): Promise => {
+ try {
+ const episode = await prisma.watchListEpisode.findMany({
+ where: {
+ AND: [
+ {
+ userProfileId: name,
+ },
+ {
+ watchId: {
+ equals: id,
+ },
+ },
+ ],
+ },
+ });
+ return episode;
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error getting episode");
+ }
+};
+
+export const updateUserEpisode = async ({
+ name,
+ id,
+ watchId,
+ title,
+ image,
+ number,
+ duration,
+ timeWatched,
+ aniTitle,
+ provider,
+ nextId,
+ nextNumber,
+ dub,
+}: UpdateUserEpisodeParams) => {
+ try {
+ await prisma.watchListEpisode.updateMany({
+ where: {
+ userProfileId: name,
+ watchId: watchId,
+ },
+ data: {
+ title: title,
+ aniTitle: aniTitle,
+ image: image,
+ aniId: id,
+ provider: provider,
+ duration: duration,
+ episode: number,
+ timeWatched: timeWatched,
+ nextId: nextId,
+ nextNumber: nextNumber,
+ dub: dub,
+ createdDate: new Date(),
+ },
+ });
+
+ // return user;
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error updating user episode");
+ }
+};
+
+export const deleteEpisode = async (
+ name: string,
+ id: string
+): Promise<{ success?: boolean; message?: string } | null> => {
+ try {
+ const user = await prisma.watchListEpisode.deleteMany({
+ where: {
+ watchId: id,
+ userProfileId: name,
+ },
+ });
+ if (user) {
+ return { success: true };
+ } else {
+ return { message: "Episode not found" };
+ }
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error deleting episode");
+ }
+};
+
+export const deleteList = async (
+ name: string,
+ id: string
+): Promise<{ success?: boolean; message?: string } | null> => {
+ try {
+ const user = await prisma.watchListEpisode.deleteMany({
+ where: {
+ aniId: id,
+ userProfileId: name,
+ },
+ });
+ if (user) {
+ return { success: true };
+ } else {
+ return { message: "Episode not found" };
+ }
+ } catch (error) {
+ console.error(error);
+ throw new Error("Error deleting list");
+ }
+};
+
+// export const updateTimeWatched = async (id, timeWatched) => {
+// try {
+// const user = await prisma.watchListEpisode.update({
+// where: {
+// id: id,
+// },
+// data: {
+// timeWatched: timeWatched,
+// },
+// });
+// return user;
+// } catch (error) {
+// console.error(error);
+// throw new Error("Error updating time watched");
+// }
+// };
diff --git a/public/icon-144x144.png b/public/icon-144x144.png
new file mode 100644
index 0000000..d2430c6
Binary files /dev/null and b/public/icon-144x144.png differ
diff --git a/public/manifest.json b/public/manifest.json
index cd77de2..5fe3e5d 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -8,6 +8,12 @@
"short_name": "moopa",
"description": "Watch and Read your favorite Anime/Manga in one single app",
"icons": [
+ {
+ "src": "/icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "any"
+ },
{
"src": "/icon-192x192.png",
"sizes": "192x192",
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..2c3f889
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow: /en/anime/
+Disallow: /en/manga/
+Disallow: /admin/
+Disallow: /api/
\ No newline at end of file
diff --git a/styles/globals.css b/styles/globals.css
index 17ca472..10645f0 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -13,6 +13,11 @@ html {
-webkit-tap-highlight-color: transparent;
}
+:root {
+ --media-brand: 245 245 245;
+ --media-focus: 78 156 246;
+}
+
body {
@apply bg-primary scrollbar-hide text-txt;
}
@@ -413,7 +418,8 @@ pre code {
.next-button {
position: relative;
- @apply xs:w-28 xs:h-9 w-24 h-7 rounded-md font-karla shadow-xl text-black xs:text-[15px] text-xs md:text-sm flex-center hover:bg-[#b9b9b9];
+ @apply px-4 py-2 font-karla text-primary hover:bg-white/80 font-semibold;
+ /* @apply xs:w-28 xs:h-9 w-24 h-7 rounded-md font-karla shadow-xl text-black xs:text-[15px] text-xs md:text-sm flex-center hover:bg-[#b9b9b9]; */
background: #ffffff;
border-radius: 6px;
cursor: pointer;
@@ -432,7 +438,7 @@ pre code {
border-radius: 6px;
}
.next-button.progress::before {
- animation: progress 7s ease forwards;
+ animation: progress 7s linear forwards;
}
@keyframes progress {
0% {
@@ -557,3 +563,27 @@ pre code {
left: unset;
}
}
+
+[data-media-player] {
+ height: 100%;
+ display: block;
+}
+
+[data-media-provider] {
+ height: 100%;
+ border: none;
+}
+
+[data-media-provider] video {
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+.chat {
+ @apply flex flex-col gap-[10px];
+}
+
+.chat > span {
+ @apply font-karla w-full italic text-white/70;
+}
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
new file mode 100644
index 0000000..e072608
--- /dev/null
+++ b/tailwind.config.cjs
@@ -0,0 +1,94 @@
+/** @type {import('tailwindcss').Config} */
+const defaultTheme = require("tailwindcss/defaultTheme");
+const scrollbarPlugin = require("tailwind-scrollbar");
+
+module.exports = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx}",
+ "./components/**/*.{js,ts,jsx,tsx}",
+ ],
+ darkMode: "class",
+ theme: {
+ screens: {
+ xxs: "375px",
+ xs: "425px",
+
+ ...defaultTheme.screens,
+ },
+ extend: {
+ animation: {
+ text: "text 5s ease infinite",
+ },
+ keyframes: {
+ text: {
+ "0%, 100%": {
+ "background-size": "200% 200%",
+ "background-position": "left center",
+ },
+ "50%": {
+ "background-size": "200% 200%",
+ "background-position": "right center",
+ },
+ },
+ },
+ boxShadow: {
+ menu: "0 0 10px 0px rgba(255, 107, 0, 0.1)",
+ light: "0 2px 10px 2px rgba(0, 0, 0, 0.1)",
+ button: "0 0px 5px 0.5px rgba(0, 0, 0, 0.1)",
+ },
+ textColor: {
+ "gray-500": "#6c757d",
+ },
+ fontWeight: {
+ bold: "700",
+ },
+ padding: {
+ nav: "5.3rem",
+ },
+ colors: {
+ primary: "#141519",
+ secondary: "#212127",
+ action: "#FF7F57",
+ image: "#3B3C41",
+ txt: "#dbdcdd",
+ tersier: "#0c0d10",
+ },
+ },
+ fontFamily: {
+ outfit: ["Outfit", "sans-serif"],
+ karla: ["Karla", "sans-serif"],
+ roboto: ["Roboto", "sans-serif"],
+ inter: ["Inter", "sans-serif"],
+ },
+ },
+ variants: {
+ extend: {
+ display: ["group-focus"],
+ opacity: ["group-focus"],
+ inset: ["group-focus"],
+ backgroundImage: ["dark"],
+ },
+ textColor: ["responsive", "hover", "focus"],
+ fontWeight: ["responsive", "hover", "focus"],
+ scrollbar: ["rounded"],
+ },
+ plugins: [
+ scrollbarPlugin({
+ nocompatible: true,
+ }),
+ require("tailwind-scrollbar-hide"),
+ require("@vidstack/react/tailwind.cjs")({
+ // Change the media variants prefix.
+ prefix: "media",
+ }),
+ require("tailwindcss-animate"),
+ customVariants,
+ ],
+};
+
+function customVariants({ addVariant, matchVariant }) {
+ // Strict version of `.group` to help with nesting.
+ matchVariant("parent-data", (value) => `.parent[data-${value}] > &`);
+ addVariant("hocus", ["&:hover", "&:focus-visible"]);
+ addVariant("group-hocus", [".group:hover &", ".group:focus-visible &"]);
+}
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index 13e9999..0000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-const defaultTheme = require("tailwindcss/defaultTheme");
-const scrollbarPlugin = require("tailwind-scrollbar");
-
-module.exports = {
- content: [
- "./pages/**/*.{js,ts,jsx,tsx}",
- "./components/**/*.{js,ts,jsx,tsx}",
- ],
- darkMode: "class",
- theme: {
- screens: {
- xxs: "375px",
- xs: "425px",
-
- ...defaultTheme.screens,
- },
- extend: {
- animation: {
- text: "text 5s ease infinite",
- },
- keyframes: {
- text: {
- "0%, 100%": {
- "background-size": "200% 200%",
- "background-position": "left center",
- },
- "50%": {
- "background-size": "200% 200%",
- "background-position": "right center",
- },
- },
- },
- boxShadow: {
- menu: "0 0 10px 0px rgba(255, 107, 0, 0.1)",
- light: "0 2px 10px 2px rgba(0, 0, 0, 0.1)",
- button: "0 0px 5px 0.5px rgba(0, 0, 0, 0.1)",
- },
- textColor: {
- "gray-500": "#6c757d",
- },
- fontWeight: {
- bold: "700",
- },
- padding: {
- nav: "5.3rem",
- },
- colors: {
- primary: "#141519",
- secondary: "#212127",
- action: "#FF7F57",
- image: "#3B3C41",
- txt: "#dbdcdd",
- tersier: "#0c0d10",
- },
- },
- fontFamily: {
- outfit: ["Outfit", "sans-serif"],
- karla: ["Karla", "sans-serif"],
- roboto: ["Roboto", "sans-serif"],
- inter: ["Inter", "sans-serif"],
- },
- },
- variants: {
- extend: {
- display: ["group-focus"],
- opacity: ["group-focus"],
- inset: ["group-focus"],
- backgroundImage: ["dark"],
- },
- textColor: ["responsive", "hover", "focus"],
- fontWeight: ["responsive", "hover", "focus"],
- scrollbar: ["rounded"],
- },
- plugins: [
- scrollbarPlugin({
- nocompatible: true,
- }),
- require("tailwind-scrollbar-hide"),
- ],
-};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2838c72
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "baseUrl": ".",
+ "paths": {
+ "@/components/*": ["components/*"],
+ "@/utils/*": ["utils/*"],
+ "@/lib/*": ["lib/*"],
+ "@/prisma/*": ["prisma/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "pages/api/v2/episode/[id].tsx",
+ "utils/schedulesUtils.ts",
+ "pages/middleware.js",
+ "pages/api/auth/[...nextauth].ts",
+ "components/anime/mobile/reused/infoChip.tsx"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/types/api/AnifyEpisode.ts b/types/api/AnifyEpisode.ts
new file mode 100644
index 0000000..93b7e5d
--- /dev/null
+++ b/types/api/AnifyEpisode.ts
@@ -0,0 +1,16 @@
+export interface AnifyEpisode {
+ providerId: string;
+ episodes: Episode[];
+}
+
+export interface Episode {
+ id: string;
+ number: number;
+ title: string;
+ isFiller: boolean;
+ img: null;
+ hasDub: boolean;
+ description: null;
+ rating: null;
+ updatedAt: number;
+}
diff --git a/types/api/ConsumetInfo.ts b/types/api/ConsumetInfo.ts
new file mode 100644
index 0000000..946f64a
--- /dev/null
+++ b/types/api/ConsumetInfo.ts
@@ -0,0 +1,154 @@
+export interface ConsumetInfo {
+ message: string;
+ length: number;
+ id: string;
+ title: Title;
+ malId: number;
+ synonyms: string[];
+ isLicensed: boolean;
+ isAdult: boolean;
+ countryOfOrigin: string;
+ trailer: Trailer;
+ image: string;
+ popularity: number;
+ color: string;
+ cover: string;
+ description: string;
+ status: Status;
+ releaseDate: number;
+ startDate: EndDateClass;
+ endDate: EndDateClass;
+ totalEpisodes: number;
+ currentEpisode: number;
+ rating: number;
+ duration: number;
+ genres: string[];
+ season: string;
+ studios: string[];
+ subOrDub: string;
+ type: RecommendationType;
+ recommendations: Ation[];
+ characters: Character[];
+ relations: Ation[];
+ mappings: Mapping[];
+ artwork: Artwork[];
+ episodes: Episode[];
+}
+
+export interface Artwork {
+ img: string;
+ type: ArtworkType;
+ providerId: ProviderID;
+}
+
+export enum ProviderID {
+ Anilist = "anilist",
+ Mal = "mal",
+ Tvdb = "tvdb",
+}
+
+export enum ArtworkType {
+ Banner = "banner",
+ ClearArt = "clear_art",
+ ClearLogo = "clear_logo",
+ Icon = "icon",
+ Poster = "poster",
+ TopBanner = "top_banner",
+}
+
+export interface Character {
+ id: number;
+ role: Role;
+ name: Name;
+ image: string;
+ voiceActors: VoiceActor[];
+}
+
+export interface Name {
+ first: string;
+ last: null | string;
+ full: string;
+ native: null | string;
+ userPreferred: string;
+}
+
+export enum Role {
+ Main = "MAIN",
+ Supporting = "SUPPORTING",
+}
+
+export interface VoiceActor {
+ id: number;
+ language: Language;
+ name: Name;
+ image: string;
+}
+
+export enum Language {
+ English = "English",
+ French = "French",
+ German = "German",
+ Japanese = "Japanese",
+ Portuguese = "Portuguese",
+ Spanish = "Spanish",
+}
+
+export interface EndDateClass {
+ year: number;
+ month: number;
+ day: number;
+}
+
+export interface Episode {
+ id: string;
+ title: string;
+ description: null;
+ number: number;
+ image: string;
+ airDate: null;
+}
+
+export interface Mapping {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+
+export interface Ation {
+ id: number;
+ malId: number;
+ title: Title;
+ status: Status;
+ episodes: number | null;
+ image: string;
+ cover: string;
+ rating: number;
+ type: RecommendationType;
+ relationType?: string;
+ color?: string;
+}
+
+export enum Status {
+ Completed = "Completed",
+}
+
+export interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+ userPreferred?: string;
+}
+
+export enum RecommendationType {
+ Manga = "MANGA",
+ Ona = "ONA",
+ Tv = "TV",
+ TvShort = "TV_SHORT",
+}
+
+export interface Trailer {
+ id: string;
+ site: string;
+ thumbnail: string;
+}
diff --git a/types/api/Episode.ts b/types/api/Episode.ts
new file mode 100644
index 0000000..1a7440b
--- /dev/null
+++ b/types/api/Episode.ts
@@ -0,0 +1,19 @@
+export interface EpisodeData {
+ map?: boolean;
+ providerId: string;
+ episodes: Episode[];
+}
+
+export interface Episode {
+ id: string;
+ title: string;
+ img: string;
+ number: number;
+ createdAt?: Date;
+ description: string;
+ url?: string;
+ isFiller?: boolean;
+ hasDub?: boolean;
+ rating?: null;
+ updatedAt?: number;
+}
diff --git a/types/episodes/AnifyRecentEpisode.ts b/types/episodes/AnifyRecentEpisode.ts
new file mode 100644
index 0000000..b4dc466
--- /dev/null
+++ b/types/episodes/AnifyRecentEpisode.ts
@@ -0,0 +1,91 @@
+export interface AnifyRecentEpisode {
+ id: string;
+ slug: string;
+ coverImage: string;
+ bannerImage: string;
+ trailer: string;
+ status: string;
+ season: string;
+ title: Title;
+ currentEpisode: number;
+ mappings?: MappingsEntity[] | null;
+ synonyms?: string[] | null;
+ countryOfOrigin: string;
+ description: string;
+ duration: number;
+ color: string;
+ year: number;
+ rating: RatingOrPopularity;
+ popularity: RatingOrPopularity;
+ type: string;
+ format: string;
+ relations?: RelationsEntity[] | null;
+ totalEpisodes: number;
+ genres?: string[] | null;
+ tags?: string[] | null;
+ episodes: Episodes;
+ averageRating: number;
+ averagePopularity: number;
+ artwork?: ArtworkEntity[] | null;
+ characters?: CharactersEntity[] | null;
+}
+export interface Title {
+ native: string;
+ romaji: string;
+ english: string;
+}
+export interface MappingsEntity {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+export interface RatingOrPopularity {
+ anidb: number;
+ anilist: number;
+}
+export interface RelationsEntity {
+ id: string;
+ type: string;
+ title: Title;
+ format: string;
+ relationType: string;
+}
+export interface Episodes {
+ data?: DataEntity[] | null;
+ latest: Latest;
+}
+export interface DataEntity {
+ episodes?: EpisodesEntity[] | null;
+ providerId: string;
+}
+export interface EpisodesEntity {
+ id: string;
+ img?: null;
+ title: string;
+ hasDub: boolean;
+ number: number;
+ rating?: null;
+ isFiller: boolean;
+ updatedAt: number;
+ description?: null;
+}
+export interface Latest {
+ updatedAt: number;
+ latestTitle: string;
+ latestEpisode: number;
+}
+export interface ArtworkEntity {
+ img: string;
+ type: string;
+ providerId: string;
+}
+export interface CharactersEntity {
+ name: string;
+ image: string;
+ voiceActor: VoiceActor;
+}
+export interface VoiceActor {
+ name: string;
+ image: string;
+}
diff --git a/types/episodes/ConsumetInfo.ts b/types/episodes/ConsumetInfo.ts
new file mode 100644
index 0000000..811e90b
--- /dev/null
+++ b/types/episodes/ConsumetInfo.ts
@@ -0,0 +1,126 @@
+// Consumets types
+export interface ConsumetInfo {
+ id: string;
+ title: Title;
+ malId: number;
+ synonyms?: null[] | null;
+ isLicensed: boolean;
+ isAdult: boolean;
+ countryOfOrigin: string;
+ trailer: Trailer;
+ image: string;
+ popularity: number;
+ color: string;
+ cover: string;
+ description: string;
+ status: string;
+ releaseDate: number;
+ startDate: StartDateOrEndDate;
+ endDate: StartDateOrEndDate;
+ totalEpisodes: number;
+ currentEpisode: number;
+ rating: number;
+ duration: number;
+ genres?: string[] | null;
+ season: string;
+ studios?: string[] | null;
+ subOrDub: string;
+ type: string;
+ recommendations?: RecommendationsEntity[] | null;
+ characters?: CharactersEntityConsumet[] | null;
+ relations?: RelationsEntityConsumet[] | null;
+ mappings?: MappingsEntity[] | null;
+ artwork?: ArtworkEntity[] | null;
+ episodes?: EpisodesEntity[] | null;
+}
+export interface Trailer {
+ id: string;
+ site: string;
+ thumbnail: string;
+}
+export interface StartDateOrEndDate {
+ year: number;
+ month: number;
+ day: number;
+}
+export interface RecommendationsEntity {
+ id: number;
+ malId: number;
+ title: Title1;
+ status: string;
+ episodes: number;
+ image: string;
+ cover: string;
+ rating: number;
+ type: string;
+}
+export interface Title1 {
+ romaji: string;
+ english?: string | null;
+ native: string;
+ userPreferred: string;
+}
+export interface Title {
+ native: string;
+ romaji: string;
+ english: string;
+}
+export interface CharactersEntityConsumet {
+ id: number;
+ role: string;
+ name: Name;
+ image: string;
+ voiceActors?: VoiceActorsEntity[] | null;
+}
+export interface Name {
+ first: string;
+ last?: string | null;
+ full: string;
+ native: string;
+ userPreferred: string;
+}
+export interface VoiceActorsEntity {
+ id: number;
+ language: string;
+ name: Name1;
+ image: string;
+}
+export interface Name1 {
+ first: string;
+ last: string;
+ full: string;
+ native?: string | null;
+ userPreferred: string;
+}
+export interface RelationsEntityConsumet {
+ id: number;
+ relationType: string;
+ malId: number;
+ title: Title1;
+ status: string;
+ episodes?: number | null;
+ image: string;
+ color: string;
+ type: string;
+ cover: string;
+ rating: number;
+}
+export interface MappingsEntity {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+export interface ArtworkEntity {
+ img: string;
+ type: string;
+ providerId: string;
+}
+export interface EpisodesEntity {
+ id: string;
+ title: string;
+ description?: null;
+ number: number;
+ image: string;
+ airDate?: null;
+}
diff --git a/types/episodes/Sessions.ts b/types/episodes/Sessions.ts
new file mode 100644
index 0000000..cb92fe7
--- /dev/null
+++ b/types/episodes/Sessions.ts
@@ -0,0 +1,30 @@
+export interface Sessions {
+ user: User;
+ expires: string;
+}
+
+export interface User {
+ name: string;
+ picture: Picture;
+ sub: string;
+ token: string;
+ id: number;
+ image: Image;
+ list: string[];
+ version: string;
+ iat: number;
+ exp: number;
+ jti: string;
+}
+
+export interface Picture {
+ large: string;
+ medium: string;
+ __typename: string;
+}
+
+export interface Image {
+ large: string;
+ medium: string;
+ __typename: string;
+}
diff --git a/types/episodes/TrackData.ts b/types/episodes/TrackData.ts
new file mode 100644
index 0000000..143ea23
--- /dev/null
+++ b/types/episodes/TrackData.ts
@@ -0,0 +1,70 @@
+export type TrackData = {
+ provider: string;
+ defaultQuality: DefaultQuality;
+ subtitles: Subtitle[];
+ thumbnails: string;
+ epiData: EpiData;
+ skip: Skip;
+};
+
+export interface DefaultQuality {
+ url: string;
+ headers: Headers;
+}
+
+export interface Headers {}
+
+export interface Subtitle {
+ src: string;
+ label: string;
+ kind: "subtitles" | "captions" | "descriptions" | "chapters" | "metadata";
+ default?: boolean;
+}
+
+export interface EpiData {
+ sources: Source[];
+ subtitles: Subtitle2[];
+ audio: any[];
+ intro: Intro;
+ outro: Outro;
+ headers: Headers2;
+}
+
+export interface Source {
+ url: string;
+ quality: string;
+}
+
+export interface Subtitle2 {
+ url: string;
+ lang: string;
+}
+
+export interface Intro {
+ start: number;
+ end: number;
+}
+
+export interface Outro {
+ start: number;
+ end: number;
+}
+
+export interface Headers2 {}
+
+export interface Skip {
+ op: Op;
+ ed: any;
+}
+
+export interface Op {
+ interval: Interval;
+ skipType: string;
+ skipId: string;
+ episodeLength: number;
+}
+
+export interface Interval {
+ startTime: number;
+ endTime: number;
+}
diff --git a/types/index.tsx b/types/index.tsx
new file mode 100644
index 0000000..36f6aba
--- /dev/null
+++ b/types/index.tsx
@@ -0,0 +1,17 @@
+import { AnifyEpisode } from "./api/AnifyEpisode";
+import { EpisodeData } from "./api/Episode";
+import { ConsumetInfo as APIConsumetInfo } from "./api/ConsumetInfo";
+import { AnifyRecentEpisode } from "./episodes/AnifyRecentEpisode";
+import { ConsumetInfo } from "./episodes/ConsumetInfo";
+import { Sessions } from "./episodes/Sessions";
+import { TrackData } from "./episodes/TrackData";
+
+export type {
+ AnifyEpisode,
+ EpisodeData,
+ APIConsumetInfo,
+ ConsumetInfo,
+ AnifyRecentEpisode,
+ Sessions,
+ TrackData,
+};
diff --git a/types/info/AnifySearchAdvanceTypes.ts b/types/info/AnifySearchAdvanceTypes.ts
new file mode 100644
index 0000000..18bc108
--- /dev/null
+++ b/types/info/AnifySearchAdvanceTypes.ts
@@ -0,0 +1,87 @@
+export type AnifySearchAdvanceTypes = {
+ total: number;
+ lastPage: number;
+ results: Array<{
+ id: string;
+ slug: string;
+ coverImage: string;
+ bannerImage: string;
+ status: string;
+ title: {
+ native: string;
+ romaji: string;
+ english: string;
+ };
+ duration: number;
+ mappings: Array<{
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+ }>;
+ synonyms: Array;
+ countryOfOrigin: string;
+ description: string;
+ color: string;
+ year: number;
+ rating: {
+ comick: number;
+ anilist: number;
+ };
+ popularity: {
+ comick: number;
+ anilist: number;
+ };
+ type: string;
+ format: string;
+ relations: Array<{
+ id: string;
+ type: string;
+ title: {
+ native: string;
+ romaji: string;
+ english: string;
+ };
+ format: string;
+ relationType: string;
+ }>;
+ currentChapter: any;
+ totalChapters: number;
+ totalVolumes: number;
+ genres: Array;
+ tags: Array;
+ chapters: {
+ data: Array<{
+ chapters: Array<{
+ id: string;
+ title: string;
+ number: number;
+ rating: any;
+ mixdrop?: string;
+ updatedAt: number;
+ }>;
+ providerId: string;
+ }>;
+ latest: {
+ updatedAt: number;
+ latestTitle: string;
+ latestChapter: number;
+ };
+ };
+ averageRating: number;
+ averagePopularity: number;
+ artwork: Array<{
+ img: string;
+ type: string;
+ providerId: string;
+ }>;
+ characters: Array<{
+ name: string;
+ image: string;
+ voiceActor: {
+ name: any;
+ image: any;
+ };
+ }>;
+ }>;
+};
diff --git a/types/info/AnilistInfoTypes.ts b/types/info/AnilistInfoTypes.ts
new file mode 100644
index 0000000..f6cfb6f
--- /dev/null
+++ b/types/info/AnilistInfoTypes.ts
@@ -0,0 +1,138 @@
+export interface AniListInfoTypes {
+ mediaListEntry: MediaListEntry;
+ id: number;
+ type: string;
+ format: string;
+ title: Title;
+ coverImage: CoverImage;
+ startDate: StartDate;
+ bannerImage: string;
+ description: string;
+ episodes: any;
+ nextAiringEpisode: any;
+ averageScore: number;
+ popularity: number;
+ status: string;
+ genres: string[];
+ season: any;
+ studios: Studios;
+ seasonYear: any;
+ duration: any;
+ relations: Relations;
+ recommendations: Recommendations;
+ characters: Characters;
+}
+
+interface Studios {
+ edges: Studio[];
+}
+
+interface Studio {
+ isMain: boolean;
+ node: Node4;
+}
+
+interface Node4 {
+ id: number;
+ name: string;
+}
+
+export interface MediaListEntry {
+ status: string;
+ progress: number;
+ progressVolumes: number;
+}
+
+export interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+}
+
+export interface CoverImage {
+ extraLarge: string;
+ large: string;
+ color: string;
+}
+
+export interface StartDate {
+ year: number;
+ month: number;
+}
+
+export interface Relations {
+ edges: Edge[];
+}
+
+export interface Edge {
+ id: number;
+ relationType: string;
+ node: Node;
+}
+
+export interface Node {
+ id: number;
+ title: Title2;
+ format: string;
+ type: string;
+ status: string;
+ bannerImage?: string;
+ coverImage: CoverImage2;
+}
+
+export interface Title2 {
+ userPreferred: string;
+}
+
+export interface CoverImage2 {
+ extraLarge: string;
+ color: string;
+}
+
+export interface Recommendations {
+ nodes: Node2[];
+}
+
+export interface Node2 {
+ mediaRecommendation: MediaRecommendation;
+}
+
+export interface MediaRecommendation {
+ id: number;
+ title: Title3;
+ coverImage: CoverImage3;
+}
+
+export interface Title3 {
+ romaji: string;
+}
+
+export interface CoverImage3 {
+ extraLarge: string;
+ large: string;
+}
+
+export interface Characters {
+ edges: Edge2[];
+}
+
+export interface Edge2 {
+ role: string;
+ node: Node3;
+}
+
+export interface Node3 {
+ id: number;
+ image: Image;
+ name: Name;
+}
+
+export interface Image {
+ large: string;
+ medium: string;
+}
+
+export interface Name {
+ full: string;
+ userPreferred: string;
+}
diff --git a/utils/appendMetaToEpisodes.js b/utils/appendMetaToEpisodes.js
deleted file mode 100644
index 197788b..0000000
--- a/utils/appendMetaToEpisodes.js
+++ /dev/null
@@ -1,28 +0,0 @@
-async function appendMetaToEpisodes(episodesData, images) {
- // Create a dictionary for faster lookup of images based on episode number
- const episodeImages = {};
- images.forEach((image) => {
- episodeImages[image.number || image.episode] = image;
- });
-
- // Iterate through each provider's episodes data
- for (const providerEpisodes of episodesData) {
- // Iterate through each episode in the provider's episodes data
- for (const episode of providerEpisodes.episodes) {
- // Get the episode number
- const episodeNumber = episode.number;
-
- // Check if there is an image available for this episode number
- if (episodeImages[episodeNumber]) {
- // Append the image URL to the episode data
- episode.img = episodeImages[episodeNumber].img;
- episode.title = episodeImages[episodeNumber].title;
- episode.description = episodeImages[episodeNumber].description;
- }
- }
- }
-
- return episodesData;
-}
-
-export default appendMetaToEpisodes;
diff --git a/utils/appendMetaToEpisodes.ts b/utils/appendMetaToEpisodes.ts
new file mode 100644
index 0000000..5f74df3
--- /dev/null
+++ b/utils/appendMetaToEpisodes.ts
@@ -0,0 +1,51 @@
+type Image = {
+ number?: number;
+ episode?: number;
+ img: string;
+ title: string;
+ description: string;
+};
+
+type Episode = {
+ number: number;
+ img?: string;
+ title?: string;
+ description?: string;
+};
+
+type ProviderEpisodes = {
+ episodes: Episode[];
+};
+
+async function appendMetaToEpisodes(
+ episodesData: ProviderEpisodes[],
+ images: Image[]
+): Promise {
+ // Create a dictionary for faster lookup of images based on episode number
+ const episodeImages: { [key: number]: Image } = {};
+ images.forEach((image) => {
+ image.episode && (episodeImages[image.episode] = image);
+ image.number && (episodeImages[image.number] = image);
+ });
+
+ // Iterate through each provider's episodes data
+ for (const providerEpisodes of episodesData) {
+ // Iterate through each episode in the provider's episodes data
+ for (const episode of providerEpisodes.episodes) {
+ // Get the episode number
+ const episodeNumber = episode.number;
+
+ // Check if there is an image available for this episode number
+ if (episodeImages[episodeNumber]) {
+ // Append the image URL to the episode data
+ episode.img = episodeImages[episodeNumber].img;
+ episode.title = episodeImages[episodeNumber].title;
+ episode.description = episodeImages[episodeNumber].description;
+ }
+ }
+ }
+
+ return episodesData;
+}
+
+export default appendMetaToEpisodes;
diff --git a/utils/combineImages.js b/utils/combineImages.js
deleted file mode 100644
index abf34ed..0000000
--- a/utils/combineImages.js
+++ /dev/null
@@ -1,26 +0,0 @@
-async function appendImagesToEpisodes(episodesData, images) {
- // Create a dictionary for faster lookup of images based on episode number
- const episodeImages = {};
- images.forEach((image) => {
- episodeImages[image.episode] = image.img;
- });
-
- // Iterate through each provider's episodes data
- for (const providerEpisodes of episodesData) {
- // Iterate through each episode in the provider's episodes data
- for (const episode of providerEpisodes.episodes) {
- // Get the episode number
- const episodeNumber = episode.number;
-
- // Check if there is an image available for this episode number
- if (episodeImages[episodeNumber]) {
- // Append the image URL to the episode data
- episode.img = episodeImages[episodeNumber];
- }
- }
- }
-
- return episodesData;
-}
-
-export default appendImagesToEpisodes;
diff --git a/utils/combineImages.ts b/utils/combineImages.ts
new file mode 100644
index 0000000..01b7ef3
--- /dev/null
+++ b/utils/combineImages.ts
@@ -0,0 +1,43 @@
+interface Image {
+ episode: number;
+ img: string;
+}
+
+interface Episode {
+ number: number;
+ img?: string;
+}
+
+interface ProviderEpisodes {
+ episodes: Episode[];
+}
+
+async function appendImagesToEpisodes(
+ episodesData: ProviderEpisodes[],
+ images: Image[]
+) {
+ // Create a dictionary for faster lookup of images based on episode number
+ const episodeImages: { [key: number]: string } = {};
+ images.forEach((image) => {
+ episodeImages[image.episode] = image.img;
+ });
+
+ // Iterate through each provider's episodes data
+ for (const providerEpisodes of episodesData) {
+ // Iterate through each episode in the provider's episodes data
+ for (const episode of providerEpisodes.episodes) {
+ // Get the episode number
+ const episodeNumber = episode.number;
+
+ // Check if there is an image available for this episode number
+ if (episodeImages[episodeNumber]) {
+ // Append the image URL to the episode data
+ episode.img = episodeImages[episodeNumber];
+ }
+ }
+ }
+
+ return episodesData;
+}
+
+export default appendImagesToEpisodes;
diff --git a/utils/getFormat.js b/utils/getFormat.js
deleted file mode 100644
index 9a2e3e3..0000000
--- a/utils/getFormat.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const data = [
- { name: "TV Show", value: "TV" },
- { name: "TV Short", value: "TV_SHORT" },
- { name: "Movie", value: "MOVIE" },
- { name: "Special", value: "SPECIAL" },
- { name: "OVA", value: "OVA" },
- { name: "ONA", value: "ONA" },
- { name: "Music", value: "MUSIC" },
- { name: "Manga", value: "MANGA" },
- { name: "Novel", value: "NOVEL" },
- { name: "One Shot", value: "ONE_SHOT" },
-];
-
-export function getFormat(format) {
- const results = data.find((item) => item.value === format);
- return results?.name;
-}
diff --git a/utils/getFormat.ts b/utils/getFormat.ts
new file mode 100644
index 0000000..7f3eece
--- /dev/null
+++ b/utils/getFormat.ts
@@ -0,0 +1,17 @@
+const data = [
+ { name: "TV Show", value: "TV" },
+ { name: "TV Short", value: "TV_SHORT" },
+ { name: "Movie", value: "MOVIE" },
+ { name: "Special", value: "SPECIAL" },
+ { name: "OVA", value: "OVA" },
+ { name: "ONA", value: "ONA" },
+ { name: "Music", value: "MUSIC" },
+ { name: "Manga", value: "MANGA" },
+ { name: "Novel", value: "NOVEL" },
+ { name: "One Shot", value: "ONE_SHOT" },
+];
+
+export function getFormat(format: string) {
+ const results = data.find((item) => item.value === format);
+ return results?.name;
+}
diff --git a/utils/getGreetings.js b/utils/getGreetings.js
deleted file mode 100644
index 1dd2a53..0000000
--- a/utils/getGreetings.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export const getGreetings = () => {
- const time = new Date().getHours();
- let greeting = "";
-
- if (time >= 5 && time < 12) {
- greeting = "Good morning";
- } else if (time >= 12 && time < 18) {
- greeting = "Good afternoon";
- } else if (time >= 18 && time < 22) {
- greeting = "Good evening";
- } else if (time >= 22 || time < 5) {
- greeting = "Good night";
- }
-
- return greeting;
-};
diff --git a/utils/getGreetings.ts b/utils/getGreetings.ts
new file mode 100644
index 0000000..1dd2a53
--- /dev/null
+++ b/utils/getGreetings.ts
@@ -0,0 +1,16 @@
+export const getGreetings = () => {
+ const time = new Date().getHours();
+ let greeting = "";
+
+ if (time >= 5 && time < 12) {
+ greeting = "Good morning";
+ } else if (time >= 12 && time < 18) {
+ greeting = "Good afternoon";
+ } else if (time >= 18 && time < 22) {
+ greeting = "Good evening";
+ } else if (time >= 22 || time < 5) {
+ greeting = "Good night";
+ }
+
+ return greeting;
+};
diff --git a/utils/getRedisWithPrefix.js b/utils/getRedisWithPrefix.js
deleted file mode 100644
index b85589b..0000000
--- a/utils/getRedisWithPrefix.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { redis } from "@/lib/redis";
-
-export async function getValuesWithPrefix(prefix) {
- let cursor = "0"; // Start at the beginning of the keyspace
- let values = [];
-
- do {
- const [newCursor, matchingKeys] = await redis.scan(
- cursor,
- "MATCH",
- prefix + "*",
- "COUNT",
- 100
- );
-
- // Retrieve values for matching keys and add them to the array
- for (const key of matchingKeys) {
- const value = await redis.get(key);
- values.push(JSON.parse(value));
- }
-
- // Update the cursor for the next iteration
- cursor = newCursor;
- } while (cursor !== "0"); // Continue until the cursor is '0'
-
- return values;
-}
-
-export async function countKeysWithPrefix(prefix) {
- let cursor = "0"; // Start at the beginning of the keyspace
- let count = 0;
-
- do {
- const [newCursor, matchingKeys] = await redis.scan(
- cursor,
- "MATCH",
- prefix + "*",
- "COUNT",
- 100
- );
-
- // Increment the count by the number of matching keys in this iteration
- count += matchingKeys.length;
-
- // Update the cursor for the next iteration
- cursor = newCursor;
- } while (cursor !== "0"); // Continue until the cursor is '0'
-
- return count;
-}
-
-export async function getValuesWithNumericKeys() {
- const allKeys = await redis.keys("*"); // Fetch all keys in Redis
- const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
-
- const values = [];
-
- for (const key of numericKeys) {
- const value = await redis.get(key); // Retrieve the value for each numeric key
- values.push(value);
- }
-
- return values;
-}
-
-export async function getKeysWithNumericKeys() {
- const allKeys = await redis.keys("*"); // Fetch all keys in Redis
- const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
-
- const values = [];
-
- for (const key of numericKeys) {
- const value = await redis.del(key);
- }
-
- return values;
-}
-
-export async function countNumericKeys() {
- const allKeys = await redis.keys("*"); // Fetch all keys in Redis
- const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
-
- return numericKeys.length; // Return the count of numeric keys
-}
diff --git a/utils/getRedisWithPrefix.ts b/utils/getRedisWithPrefix.ts
new file mode 100644
index 0000000..dacf78e
--- /dev/null
+++ b/utils/getRedisWithPrefix.ts
@@ -0,0 +1,86 @@
+import { redis } from "@/lib/redis";
+
+export async function getValuesWithPrefix(prefix: string) {
+ let cursor = "0"; // Start at the beginning of the keyspace
+ let values = [];
+
+ do {
+ const [newCursor, matchingKeys] = await redis.scan(
+ cursor,
+ "MATCH",
+ prefix + "*",
+ "COUNT",
+ 100
+ );
+
+ // Retrieve values for matching keys and add them to the array
+ for (const key of matchingKeys) {
+ const value = await redis.get(key);
+ if (value !== null) {
+ values.push(JSON.parse(value));
+ }
+ }
+
+ // Update the cursor for the next iteration
+ cursor = newCursor;
+ } while (cursor !== "0"); // Continue until the cursor is '0'
+
+ return values;
+}
+
+export async function countKeysWithPrefix(prefix: string) {
+ let cursor = "0"; // Start at the beginning of the keyspace
+ let count = 0;
+
+ do {
+ const [newCursor, matchingKeys] = await redis.scan(
+ cursor,
+ "MATCH",
+ prefix + "*",
+ "COUNT",
+ 100
+ );
+
+ // Increment the count by the number of matching keys in this iteration
+ count += matchingKeys.length;
+
+ // Update the cursor for the next iteration
+ cursor = newCursor;
+ } while (cursor !== "0"); // Continue until the cursor is '0'
+
+ return count;
+}
+
+export async function getValuesWithNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ const values = [];
+
+ for (const key of numericKeys) {
+ const value = await redis.get(key); // Retrieve the value for each numeric key
+ values.push(value);
+ }
+
+ return values;
+}
+
+export async function getKeysWithNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ const values: any[] = [];
+
+ for (const key of numericKeys) {
+ await redis.del(key);
+ }
+
+ return values;
+}
+
+export async function countNumericKeys() {
+ const allKeys = await redis.keys("*"); // Fetch all keys in Redis
+ const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
+
+ return numericKeys.length; // Return the count of numeric keys
+}
diff --git a/utils/getTimes.js b/utils/getTimes.js
deleted file mode 100644
index 95df803..0000000
--- a/utils/getTimes.js
+++ /dev/null
@@ -1,143 +0,0 @@
-export function convertUnixToTime(timestamp) {
- const date = new Date(timestamp);
- const hours = date.getHours();
- const minutes = date.getMinutes();
- const ampm = hours >= 12 ? "PM" : "AM";
- const formattedHours = (hours % 12 || 12).toString().padStart(2, "0");
- const formattedMinutes = minutes.toString().padStart(2, "0");
- return `${formattedHours}:${formattedMinutes} ${ampm}`;
-}
-
-export function getCurrentSeason() {
- const now = new Date();
- const month = now.getMonth() + 1; // getMonth() returns 0-based index
-
- switch (month) {
- case 12:
- case 1:
- case 2:
- return "WINTER";
- case 3:
- case 4:
- case 5:
- return "SPRING";
- case 6:
- case 7:
- case 8:
- return "SUMMER";
- case 9:
- case 10:
- case 11:
- return "FALL";
- default:
- return "UNKNOWN SEASON";
- }
-}
-
-export function convertUnixToCountdown(time) {
- let date = new Date(time * 1000);
- let days = date.getDay();
- let hours = date.getHours();
- let minutes = date.getMinutes();
-
- let countdown = "";
-
- if (days > 0) {
- countdown += `${days}d `;
- }
-
- if (hours > 0) {
- countdown += `${hours}h `;
- }
-
- if (minutes > 0) {
- countdown += `${minutes}m `;
- }
-
- return countdown.trim();
-}
-
-export function convertSecondsToTime(sec) {
- let days = Math.floor(sec / (3600 * 24));
- let hours = Math.floor((sec % (3600 * 24)) / 3600);
- let minutes = Math.floor((sec % 3600) / 60);
- let seconds = Math.floor(sec % 60);
-
- let time = "";
-
- if (days > 0) {
- time += `${days}d `;
- }
-
- if (hours > 0) {
- time += `${hours}h `;
- }
-
- if (minutes > 0) {
- time += `${minutes}m `;
- }
-
- if (days <= 0) {
- time += `${seconds}s `;
- }
-
- return time.trim();
-}
-
-// Function to convert timestamp to AM/PM time format
-export const timeStamptoAMPM = (timestamp) => {
- const date = new Date(timestamp * 1000);
- const hours = date.getHours();
- const minutes = date.getMinutes();
- const ampm = hours >= 12 ? "PM" : "AM";
- const formattedHours = hours % 12 || 12; // Convert to 12-hour format
-
- return `${formattedHours}:${minutes.toString().padStart(2, "0")} ${ampm}`;
-};
-
-export const timeStamptoHour = (timestamp) => {
- const options = { hour: "numeric", minute: "numeric", hour12: true };
- const currentTime = new Date().getTime() / 1000;
- const formattedTime = new Date(timestamp * 1000).toLocaleTimeString(
- undefined,
- options
- );
- const status = timestamp <= currentTime ? "aired" : "airing";
-
- return `${status} at ${formattedTime}`;
-};
-
-export function unixTimestampToRelativeTime(unixTimestamp) {
- const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
- let secondsDifference = now - unixTimestamp;
-
- const intervals = [
- { label: "year", seconds: 31536000 },
- { label: "month", seconds: 2592000 },
- { label: "week", seconds: 604800 },
- { label: "day", seconds: 86400 },
- { label: "hour", seconds: 3600 },
- { label: "minute", seconds: 60 },
- { label: "second", seconds: 1 },
- ];
-
- const isFuture = secondsDifference < 0;
- secondsDifference = Math.abs(secondsDifference);
-
- for (const interval of intervals) {
- const count = Math.floor(secondsDifference / interval.seconds);
- if (count >= 1) {
- const label = count === 1 ? interval.label : `${interval.label}s`;
- return isFuture ? `${count} ${label} from now` : `${count} ${label} ago`;
- }
- }
-
- return "just now";
-}
-
-export function unixToSeconds(unixTimestamp) {
- const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
- const secondsAgo = now - unixTimestamp;
-
- return secondsAgo;
-}
diff --git a/utils/getTimes.ts b/utils/getTimes.ts
new file mode 100644
index 0000000..c3fe0ad
--- /dev/null
+++ b/utils/getTimes.ts
@@ -0,0 +1,159 @@
+export function convertUnixToTime(timestamp: number) {
+ const date = new Date(timestamp);
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? "PM" : "AM";
+ const formattedHours = (hours % 12 || 12).toString().padStart(2, "0");
+ const formattedMinutes = minutes.toString().padStart(2, "0");
+ return `${formattedHours}:${formattedMinutes} ${ampm}`;
+}
+
+export function getCurrentSeason() {
+ const now = new Date();
+ const month = now.getMonth() + 1; // getMonth() returns 0-based index
+
+ switch (month) {
+ case 12:
+ case 1:
+ case 2:
+ return "WINTER";
+ case 3:
+ case 4:
+ case 5:
+ return "SPRING";
+ case 6:
+ case 7:
+ case 8:
+ return "SUMMER";
+ case 9:
+ case 10:
+ case 11:
+ return "FALL";
+ default:
+ return "UNKNOWN SEASON";
+ }
+}
+
+export function convertUnixToCountdown(time: number) {
+ let date = new Date(time * 1000);
+ let days = date.getDay();
+ let hours = date.getHours();
+ let minutes = date.getMinutes();
+
+ let countdown = "";
+
+ if (days > 0) {
+ countdown += `${days}d `;
+ }
+
+ if (hours > 0) {
+ countdown += `${hours}h `;
+ }
+
+ if (minutes > 0) {
+ countdown += `${minutes}m `;
+ }
+
+ return countdown.trim();
+}
+
+export function convertSecondsToTime(sec: number) {
+ let days = Math.floor(sec / (3600 * 24));
+ let hours = Math.floor((sec % (3600 * 24)) / 3600);
+ let minutes = Math.floor((sec % 3600) / 60);
+ let seconds = Math.floor(sec % 60);
+
+ let time = "";
+
+ if (days > 0) {
+ time += `${days}d `;
+ }
+
+ if (hours > 0) {
+ time += `${hours}h `;
+ }
+
+ if (minutes > 0) {
+ time += `${minutes}m `;
+ }
+
+ if (days <= 0) {
+ time += `${seconds}s `;
+ }
+
+ return time.trim();
+}
+
+// Function to convert timestamp to AM/PM time format
+export const timeStamptoAMPM = (timestamp: number | string) => {
+ const date = new Date(Number(timestamp) * 1000);
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? "PM" : "AM";
+ const formattedHours = hours % 12 || 12; // Convert to 12-hour format
+
+ return `${formattedHours}:${minutes.toString().padStart(2, "0")} ${ampm}`;
+};
+
+export const timeStamptoHour = (timestamp: number) => {
+ const currentTime = new Date().getTime() / 1000;
+ const formattedTime = new Date(timestamp * 1000).toLocaleTimeString(
+ undefined,
+ { hour: "numeric", minute: "numeric", hour12: true }
+ );
+ const status = timestamp <= currentTime ? "aired" : "airing";
+
+ return `${status} at ${formattedTime}`;
+};
+
+export function unixTimestampToRelativeTime(unixTimestamp: number) {
+ const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
+ let secondsDifference = now - unixTimestamp;
+
+ const intervals = [
+ { label: "year", seconds: 31536000 },
+ { label: "month", seconds: 2592000 },
+ { label: "week", seconds: 604800 },
+ { label: "day", seconds: 86400 },
+ { label: "hour", seconds: 3600 },
+ { label: "minute", seconds: 60 },
+ { label: "second", seconds: 1 },
+ ];
+
+ const isFuture = secondsDifference < 0;
+ secondsDifference = Math.abs(secondsDifference);
+
+ for (const interval of intervals) {
+ const count = Math.floor(secondsDifference / interval.seconds);
+ if (count >= 1) {
+ const label = count === 1 ? interval.label : `${interval.label}s`;
+ return isFuture ? `${count} ${label} from now` : `${count} ${label} ago`;
+ }
+ }
+
+ return "just now";
+}
+
+export function unixToSeconds(unixTimestamp: number) {
+ const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
+ const secondsAgo = now - unixTimestamp;
+
+ return secondsAgo;
+}
+
+export function realTimeCountdown(secondsLeft: number): string {
+ let countdown = "";
+ const intervalId = setInterval(() => {
+ secondsLeft--;
+ const hours = Math.floor(secondsLeft / 3600);
+ const minutes = Math.floor((secondsLeft % 3600) / 60);
+ const seconds = secondsLeft % 60;
+ countdown = `${hours.toString().padStart(2, "0")}:${minutes
+ .toString()
+ .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ if (secondsLeft <= 0) {
+ clearInterval(intervalId);
+ }
+ }, 1000);
+ return countdown;
+}
diff --git a/utils/imageUtils.js b/utils/imageUtils.js
deleted file mode 100644
index 8025d5b..0000000
--- a/utils/imageUtils.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export function getHeaders(providerId) {
- switch (providerId) {
- case "mangahere":
- return { Referer: "https://mangahere.org" };
- case "mangadex":
- return { Referer: "https://mangadex.org" };
- case "mangakakalot":
- return { Referer: "https://mangakakalot.com" };
- case "mangapill":
- return { Referer: "https://mangapill.com" };
- case "mangasee123":
- return { Referer: "https://mangasee123.com" };
- case "comick":
- return { Referer: "https://comick.app" };
- default:
- return null;
- }
-}
-
-export function getRandomId() {
- return Math.random().toString(36).substr(2, 9);
-}
-
-export function truncateImgUrl(url) {
- // Find the index of .png if not found find the index of .jpg
- let index =
- url?.indexOf(".png") !== -1 ? url?.indexOf(".png") : url?.indexOf(".jpg");
-
- if (index !== -1) {
- // If .png or .jpg is found
- url = url?.slice(0, index + 4); // Slice the string from the start to the index of .png or .jpg plus 4 (the length of .png or .jpg)
- } else {
- // If .png or .jpg is not found
- return url; // Return the original url string
- }
-
- return url;
-}
diff --git a/utils/imageUtils.ts b/utils/imageUtils.ts
new file mode 100644
index 0000000..6220134
--- /dev/null
+++ b/utils/imageUtils.ts
@@ -0,0 +1,55 @@
+export function getHeaders(providerId: string) {
+ switch (providerId) {
+ case "mangahere":
+ return { Referer: "https://mangahere.org" };
+ case "mangadex":
+ return { Referer: "https://mangadex.org" };
+ case "mangakakalot":
+ return { Referer: "https://mangakakalot.com" };
+ case "mangapill":
+ return { Referer: "https://mangapill.com" };
+ case "mangasee123":
+ return { Referer: "https://mangasee123.com" };
+ case "comick":
+ return { Referer: "https://comick.app" };
+ default:
+ return null;
+ }
+}
+
+export function getRandomId() {
+ return Math.random().toString(36).substr(2, 9);
+}
+
+export function truncateImgUrl(url: string | undefined) {
+ if (!url) return null;
+
+ // Find the index of .png if not found find the index of .jpg
+ let index =
+ url?.indexOf(".png") !== -1 ? url?.indexOf(".png") : url?.indexOf(".jpg");
+
+ if (index && index !== -1) {
+ // If .png or .jpg is found
+ url = url?.slice(0, index + 4); // Slice the string from the start to the index of .png or .jpg plus 4 (the length of .png or .jpg)
+ } else {
+ // If .png or .jpg is not found
+ return url; // Return the original url string
+ }
+
+ return url;
+}
+
+export function parseImageProxy(
+ url: string | undefined | null,
+ providerId: string | undefined
+) {
+ if (!url) return;
+
+ return providerId
+ ? `https://aoi.moopa.live/utils/image-proxy?url=${truncateImgUrl(
+ url
+ )}${`&headers=${encodeURIComponent(
+ JSON.stringify({ Referer: providerId })
+ )}`}`
+ : url;
+}
diff --git a/utils/parseMetaData.ts b/utils/parseMetaData.ts
new file mode 100644
index 0000000..597c21c
--- /dev/null
+++ b/utils/parseMetaData.ts
@@ -0,0 +1,36 @@
+type Episode = {
+ id: string;
+ description: string | null;
+ hasDub: boolean;
+ img: string | null;
+ isFiller: boolean;
+ number: number;
+ rating: number | null;
+ title: string;
+ updatedAt: number;
+};
+
+type Provider = {
+ providerId: string;
+ data: Episode[];
+};
+
+export function getProviderWithMostEpisodesAndImage(
+ data: Provider[]
+): Provider | null {
+ let maxEpisodesProvider: Provider | null = null;
+
+ for (const provider of data) {
+ if (
+ !maxEpisodesProvider ||
+ provider.data.length > maxEpisodesProvider.data.length
+ ) {
+ const hasImage = provider.data.some((episode) => episode.img !== null);
+ if (hasImage) {
+ maxEpisodesProvider = provider;
+ }
+ }
+ }
+
+ return maxEpisodesProvider;
+}
diff --git a/utils/request/index.ts b/utils/request/index.ts
new file mode 100644
index 0000000..854ef2b
--- /dev/null
+++ b/utils/request/index.ts
@@ -0,0 +1,111 @@
+import axios, { AxiosRequestConfig } from "axios";
+import { getSession } from "next-auth/react";
+import { toast } from "sonner";
+
+function isAnilist(url: string | undefined): boolean {
+ return url?.includes("anilist.co") ?? false;
+}
+
+interface RequestOption extends RequestInit {
+ headers?: {
+ "Content-Type"?: string;
+ Authorization?: string;
+ };
+}
+
+const pls = {
+ // GET request handler
+ async get(
+ url: string,
+ options?: AxiosRequestConfig,
+ ctx?: any
+ ): Promise {
+ try {
+ const session: any | null = isAnilist(url) ? await getSession(ctx) : null;
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await axios.get(url, { ...options, signal });
+ return response.data;
+ } catch (error: any) {
+ handleError(error);
+ // throw error;
+ }
+ },
+
+ // POST request handler
+ async post(url: string, options: RequestOption, ctx?: any): Promise {
+ try {
+ const session: any | null = await getSession(ctx);
+ const accessToken: string | undefined = session?.user?.token;
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken &&
+ isAnilist(url) && { Authorization: `Bearer ${accessToken}` }),
+ },
+ ...options,
+ signal,
+ });
+
+ const data = await response.json();
+ return [data, session];
+ } catch (error: any) {
+ handleError(error);
+ // throw error;
+ }
+ },
+};
+
+function handleError(error: {
+ response: { status: any; data: any };
+ message: any;
+}) {
+ console.log(error);
+ if (error.response) {
+ const { status, data } = error.response;
+ switch (status) {
+ case 400:
+ toast.error("400 Bad request", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 401:
+ toast.error("401 Unauthorized", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 403:
+ toast.error("403 Forbidden", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 404:
+ toast.error(`Resource not found - 404`, {
+ description: data?.message || error.message,
+ });
+ break;
+ case 500:
+ toast.error("500 Internal server error", {
+ description: data?.message || error.message,
+ });
+ break;
+ default:
+ toast.error("An error occurred", {
+ description: data?.message || error.message,
+ });
+ break;
+ }
+
+ if (data && data.message) {
+ console.error("Error message:", data.message);
+ }
+ }
+}
+
+export default pls;
diff --git a/utils/schedulesUtils.js b/utils/schedulesUtils.js
deleted file mode 100644
index cb8c474..0000000
--- a/utils/schedulesUtils.js
+++ /dev/null
@@ -1,83 +0,0 @@
-// Function to transform the schedule data into the desired format
-export const transformSchedule = (schedule) => {
- const formattedSchedule = {};
-
- for (const day of Object.keys(schedule)) {
- formattedSchedule[day] = {};
-
- for (const scheduleItem of schedule[day]) {
- const time = scheduleItem.airingAt;
-
- if (!formattedSchedule[day][time]) {
- formattedSchedule[day][time] = [];
- }
-
- formattedSchedule[day][time].push(scheduleItem);
- }
- }
-
- return formattedSchedule;
-};
-
-export const sortScheduleByDay = (schedule) => {
- const daysOfWeek = [
- "Saturday",
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- ];
-
- // Get the current day of the week (0 = Sunday, 1 = Monday, ...)
- const currentDay = new Date().getDay();
-
- // Reorder days of the week to start with today
- const orderedDays = [
- ...daysOfWeek.slice(currentDay),
- ...daysOfWeek.slice(0, currentDay),
- ];
-
- // Create a new object with sorted days
- const sortedSchedule = {};
- orderedDays.forEach((day) => {
- if (schedule[day]) {
- sortedSchedule[day] = schedule[day];
- }
- });
-
- return sortedSchedule;
-};
-
-export const filterScheduleByDay = (sortedSchedule, filterDay) => {
- if (filterDay === "All") return sortedSchedule;
- // Create a new object to store the filtered schedules
- const filteredSchedule = {};
-
- // Iterate through the keys (days) in sortedSchedule
- for (const day in sortedSchedule) {
- // Check if the current day matches the filterDay
- if (day === filterDay) {
- // If it matches, add the schedules for that day to the filteredSchedule object
- filteredSchedule[day] = sortedSchedule[day];
- }
- }
-
- // Return the filtered schedule
- return filteredSchedule;
-};
-
-export const filterFormattedSchedule = (formattedSchedule, filterDay) => {
- if (filterDay === "All") return formattedSchedule;
-
- // Check if the selected day exists in the formattedSchedule
- if (formattedSchedule.hasOwnProperty(filterDay)) {
- return {
- [filterDay]: formattedSchedule[filterDay],
- };
- }
-
- // If the selected day does not exist, return an empty object
- return {};
-};
diff --git a/utils/schedulesUtils.ts b/utils/schedulesUtils.ts
new file mode 100644
index 0000000..606e3fa
--- /dev/null
+++ b/utils/schedulesUtils.ts
@@ -0,0 +1,96 @@
+interface ScheduleItem {
+ airingAt: string;
+ // Add other properties of ScheduleItem if available
+}
+
+interface Schedule {
+ [day: string]: ScheduleItem[];
+}
+
+interface FormattedSchedule {
+ [day: string]: {
+ [time: string]: ScheduleItem[];
+ };
+}
+
+export const transformSchedule = (schedule: Schedule): FormattedSchedule => {
+ const formattedSchedule: FormattedSchedule = {};
+
+ for (const day of Object.keys(schedule)) {
+ formattedSchedule[day] = {};
+
+ for (const scheduleItem of schedule[day]) {
+ const time = scheduleItem.airingAt;
+
+ if (!formattedSchedule[day][time]) {
+ formattedSchedule[day][time] = [];
+ }
+
+ formattedSchedule[day][time].push(scheduleItem);
+ }
+ }
+
+ return formattedSchedule;
+};
+
+export const sortScheduleByDay = (
+ schedule: FormattedSchedule
+): FormattedSchedule => {
+ const daysOfWeek: string[] = [
+ "Saturday",
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ ];
+
+ const currentDay: number = new Date().getDay();
+
+ const orderedDays: string[] = [
+ ...daysOfWeek.slice(currentDay),
+ ...daysOfWeek.slice(0, currentDay),
+ ];
+
+ const sortedSchedule: FormattedSchedule = {};
+ orderedDays.forEach((day) => {
+ if (schedule[day]) {
+ sortedSchedule[day] = schedule[day];
+ }
+ });
+
+ return sortedSchedule;
+};
+
+export const filterScheduleByDay = (
+ sortedSchedule: FormattedSchedule,
+ filterDay: string
+): FormattedSchedule => {
+ if (filterDay === "All") return sortedSchedule;
+
+ const filteredSchedule: FormattedSchedule = {};
+
+ for (const day in sortedSchedule) {
+ if (day === filterDay) {
+ filteredSchedule[day] = sortedSchedule[day];
+ }
+ }
+
+ return filteredSchedule;
+};
+
+export const filterFormattedSchedule = (
+ formattedSchedule: FormattedSchedule,
+ filterDay: string
+): FormattedSchedule => {
+ if (filterDay === "All") return formattedSchedule;
+
+ if (formattedSchedule.hasOwnProperty(filterDay)) {
+ return {
+ [filterDay]: formattedSchedule[filterDay],
+ };
+ }
+
+ return {};
+};
diff --git a/utils/useCountdownSeconds.js b/utils/useCountdownSeconds.js
deleted file mode 100644
index df3cb63..0000000
--- a/utils/useCountdownSeconds.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useEffect, useState } from "react";
-
-const useCountdown = (targetDate, update) => {
- const countDownDate = new Date(targetDate).getTime();
-
- const [countDown, setCountDown] = useState(
- countDownDate - new Date().getTime()
- );
-
- useEffect(() => {
- const interval = setInterval(() => {
- const newCountDown = countDownDate - new Date().getTime();
- setCountDown(newCountDown);
- if (newCountDown <= 0 && newCountDown > -1000) {
- update();
- }
- }, 1000);
-
- return () => clearInterval(interval);
- }, [countDownDate, update]);
-
- return getReturnValues(countDown);
-};
-
-const getReturnValues = (countDown) => {
- // calculate time left
- const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
- const hours = Math.floor(
- (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
- );
- const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
- const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
-
- return [days, hours, minutes, seconds];
-};
-
-export { useCountdown };
--
cgit v1.2.3